/*
 * 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.
  • If the current theme is derived from Theme.Theme the widget presents the current value as an editable input field with an increment button above and a decrement button below. Long pressing the buttons allows for a quick change of the current value. Tapping on the input field allows to type in a desired value.
  • If the current theme is derived from Theme_Holo.Theme_Holo or Theme_Holo_Light.Theme_Holo_Light the widget presents the current value as an editable input field with a lesser value above and a greater value below. Tapping on the lesser or greater value selects it by animating the number axis up or down to make the chosen value current. Flinging up or down allows for multiple increments or decrements of the current value. Long pressing on the lesser and greater values also allows for a quick change of the current value. Tapping on the current value allows to type in a desired value.

For an example of using this widget, see TimePicker.

/** * 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. * <ul> * <li> * If the current theme is derived from {@link android.R.style#Theme} the widget * presents the current value as an editable input field with an increment button * above and a decrement button below. Long pressing the buttons allows for a quick * change of the current value. Tapping on the input field allows to type in * a desired value. * </li> * <li> * If the current theme is derived from {@link android.R.style#Theme_Holo} or * {@link android.R.style#Theme_Holo_Light} the widget presents the current * value as an editable input field with a lesser value above and a greater * value below. Tapping on the lesser or greater value selects it by animating * the number axis up or down to make the chosen value current. Flinging up * or down allows for multiple increments or decrements of the current value. * Long pressing on the lesser and greater values also allows for a quick change * of the current value. Tapping on the current value allows to type in a * desired value. * </li> * </ul> * <p> * For an example of using this widget, see {@link android.widget.TimePicker}. * </p> */
@Widget public class NumberPicker extends LinearLayout {
The number of items show in the selector wheel.
/** * 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.
/** * The default update interval during long press. */
private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300;
The index of the middle selector item.
/** * 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.
/** * 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.
/** * 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.
/** * 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.
/** * 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.
/** * 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.
/** * 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.
/** * The resource id for the default layout. */
private static final int DEFAULT_LAYOUT_RESOURCE_ID = R.layout.number_picker;
Constant for unspecified size.
/** * Constant for unspecified size. */
private static final int SIZE_UNSPECIFIED = -1;
User choice on whether the selector wheel should be wrapped.
/** * 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().
/** * 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
/** * @hide */
public static final Formatter getTwoDigitFormatter() { return sTwoDigitFormatter; }
The increment button.
/** * The increment button. */
private final ImageButton mIncrementButton;
The decrement button.
/** * The decrement button. */
private final ImageButton mDecrementButton;
The text for showing the current value.
/** * The text for showing the current value. */
private final EditText mInputText;
The distance between the two selection dividers.
/** * The distance between the two selection dividers. */
private final int mSelectionDividersDistance;
The min height of this widget.
/** * The min height of this widget. */
private final int mMinHeight;
The max height of this widget.
/** * The max height of this widget. */
private final int mMaxHeight;
The max width of this widget.
/** * The max width of this widget. */
private final int mMinWidth;
The max width of this widget.
/** * The max width of this widget. */
private int mMaxWidth;
Flag whether to compute the max width.
/** * Flag whether to compute the max width. */
private final boolean mComputeMaxWidth;
The height of the text.
/** * The height of the text. */
private final int mTextSize;
The height of the gap between text elements if the selector wheel.
/** * The height of the gap between text elements if the selector wheel. */
private int mSelectorTextGapHeight;
The values to be displayed instead the indices.
/** * The values to be displayed instead the indices. */
private String[] mDisplayedValues;
Lower value of the range of numbers allowed for the NumberPicker
/** * 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
/** * Upper value of the range of numbers allowed for the NumberPicker */
private int mMaxValue;
Current value of this NumberPicker
/** * Current value of this NumberPicker */
private int mValue;
Listener to be notified upon current value change.
/** * Listener to be notified upon current value change. */
private OnValueChangeListener mOnValueChangeListener;
Listener to be notified upon scroll state change.
/** * Listener to be notified upon scroll state change. */
private OnScrollListener mOnScrollListener;
Formatter for for displaying the current value.
/** * Formatter for for displaying the current value. */
private Formatter mFormatter;
The speed for updating the value form long press.
/** * 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.
/** * Cache for the string representation of selector indices. */
private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>();
The selector indices whose value are show by the selector.
/** * The selector indices whose value are show by the selector. */
private final int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT];
The Paint for drawing the selector.
/** * The {@link Paint} for drawing the selector. */
private final Paint mSelectorWheelPaint;
The Drawable for pressed virtual (increment/decrement) buttons.
/** * The {@link Drawable} for pressed virtual (increment/decrement) buttons. */
private final Drawable mVirtualButtonPressedDrawable;
The height of a selector element (text + gap).
/** * The height of a selector element (text + gap). */
private int mSelectorElementHeight;
The initial offset of the scroll selector.
/** * The initial offset of the scroll selector. */
private int mInitialScrollOffset = Integer.MIN_VALUE;
The current offset of the scroll selector.
/** * The current offset of the scroll selector. */
private int mCurrentScrollOffset;
The Scroller responsible for flinging the selector.
/** * The {@link Scroller} responsible for flinging the selector. */
private final Scroller mFlingScroller;
The Scroller responsible for adjusting the selector.
/** * The {@link Scroller} responsible for adjusting the selector. */
private final Scroller mAdjustScroller;
The previous Y coordinate while scrolling the selector.
/** * The previous Y coordinate while scrolling the selector. */
private int mPreviousScrollerY;
Handle to the reusable command for setting the input text selection.
/** * Handle to the reusable command for setting the input text selection. */
private SetSelectionCommand mSetSelectionCommand;
Handle to the reusable command for changing the current value from long press by one.
/** * Handle to the reusable command for changing the current value from long * press by one. */
private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand;
Command for beginning an edit of the current value via IME on long press.
/** * Command for beginning an edit of the current value via IME on long press. */
private BeginSoftInputOnLongPressCommand mBeginSoftInputOnLongPressCommand;
The Y position of the last down event.
/** * The Y position of the last down event. */
private float mLastDownEventY;
The time of the last down event.
/** * The time of the last down event. */
private long mLastDownEventTime;
The Y position of the last down or move event.
/** * The Y position of the last down or move event. */
private float mLastDownOrMoveEventY;
Determines speed during touch scrolling.
/** * Determines speed during touch scrolling. */
private VelocityTracker mVelocityTracker;
See Also:
  • getScaledTouchSlop.getScaledTouchSlop()
/** * @see ViewConfiguration#getScaledTouchSlop() */
private int mTouchSlop;
See Also:
  • getScaledMinimumFlingVelocity.getScaledMinimumFlingVelocity()
/** * @see ViewConfiguration#getScaledMinimumFlingVelocity() */
private int mMinimumFlingVelocity;
See Also:
  • getScaledMaximumFlingVelocity.getScaledMaximumFlingVelocity()
/** * @see ViewConfiguration#getScaledMaximumFlingVelocity() */
private int mMaximumFlingVelocity;
Flag whether the selector should wrap around.
/** * Flag whether the selector should wrap around. */
private boolean mWrapSelectorWheel;
The back ground color used to optimize scroller fading.
/** * The back ground color used to optimize scroller fading. */
private final int mSolidColor;
Flag whether this widget has a selector wheel.
/** * Flag whether this widget has a selector wheel. */
private final boolean mHasSelectorWheel;
Divider for showing item to be selected while scrolling
/** * Divider for showing item to be selected while scrolling */
private final Drawable mSelectionDivider;
The height of the selection divider.
/** * The height of the selection divider. */
private final int mSelectionDividerHeight;
The current scroll state of the number picker.
/** * The current scroll state of the number picker. */
private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
Flag whether to ignore move events - we ignore such when we show in IME to prevent the content from scrolling.
/** * Flag whether to ignore move events - we ignore such when we show in IME * to prevent the content from scrolling. */
private boolean mIgnoreMoveEvents;
Flag whether to perform a click on tap.
/** * Flag whether to perform a click on tap. */
private boolean mPerformClickOnTap;
The top of the top selection divider.
/** * The top of the top selection divider. */
private int mTopSelectionDividerTop;
The bottom of the bottom selection divider.
/** * The bottom of the bottom selection divider. */
private int mBottomSelectionDividerBottom;
The virtual id of the last hovered child.
/** * The virtual id of the last hovered child. */
private int mLastHoveredChildVirtualViewId;
Whether the increment virtual button is pressed.
/** * Whether the increment virtual button is pressed. */
private boolean mIncrementVirtualButtonPressed;
Whether the decrement virtual button is pressed.
/** * Whether the decrement virtual button is pressed. */
private boolean mDecrementVirtualButtonPressed;
Provider to report to clients the semantic structure of this widget.
/** * Provider to report to clients the semantic structure of this widget. */
private AccessibilityNodeProviderImpl mAccessibilityNodeProvider;
Helper class for managing pressed state of the virtual buttons.
/** * Helper class for managing pressed state of the virtual buttons. */
private final PressedStateHelper mPressedStateHelper;
The keycode of the last handled DPAD down event.
/** * The keycode of the last handled DPAD down event. */
private int mLastHandledDownDpadKeyCode = -1;
If true then the selector wheel is hidden until the picker has focus.
/** * If true then the selector wheel is hidden until the picker has focus. */
private boolean mHideWheelUntilFocused;
Interface to listen for changes of the current value.
/** * Interface to listen for changes of the current value. */
public interface OnValueChangeListener {
Called upon a change of the current value.
Params:
  • picker – The NumberPicker associated with this listener.
  • oldVal – The previous value.
  • newVal – The new value.
/** * Called upon a change of the current value. * * @param picker The NumberPicker associated with this listener. * @param oldVal The previous value. * @param newVal The new value. */
void onValueChange(NumberPicker picker, int oldVal, int newVal); }
Interface to listen for the picker scroll state.
/** * Interface to listen for the picker scroll state. */
public interface OnScrollListener {
@hide
/** @hide */
@IntDef(prefix = { "SCROLL_STATE_" }, value = { SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING }) @Retention(RetentionPolicy.SOURCE) public @interface ScrollState {}
The view is not scrolling.
/** * The view is not scrolling. */
public static int SCROLL_STATE_IDLE = 0;
The user is scrolling using touch, and his finger is still on the screen.
/** * The user is scrolling using touch, and his finger is still on the screen. */
public static int SCROLL_STATE_TOUCH_SCROLL = 1;
The user had previously been scrolling using touch and performed a fling.
/** * The user had previously been scrolling using touch and performed a fling. */
public static int SCROLL_STATE_FLING = 2;
Callback invoked while the number picker scroll state has changed.
Params:
/** * Callback invoked while the number picker scroll state has changed. * * @param view The view whose scroll state is being reported. * @param scrollState The current scroll state. One of * {@link #SCROLL_STATE_IDLE}, * {@link #SCROLL_STATE_TOUCH_SCROLL} or * {@link #SCROLL_STATE_IDLE}. */
public void onScrollStateChange(NumberPicker view, @ScrollState int scrollState); }
Interface used to format current value into a string for presentation.
/** * Interface used to format current value into a string for presentation. */
public interface Formatter {
Formats a string representation of the current value.
Params:
  • value – The currently selected value.
Returns:A formatted string representation.
/** * Formats a string representation of the current value. * * @param value The currently selected value. * @return A formatted string representation. */
public String format(int value); }
Create a new number picker.
Params:
  • context – The application environment.
/** * Create a new number picker. * * @param context The application environment. */
public NumberPicker(Context context) { this(context, null); }
Create a new number picker.
Params:
  • context – The application environment.
  • attrs – A collection of attributes.
/** * Create a new number picker. * * @param context The application environment. * @param attrs A collection of attributes. */
public NumberPicker(Context context, AttributeSet attrs) { this(context, attrs, R.attr.numberPickerStyle); }
Create a new number picker
Params:
  • context – the application environment.
  • attrs – a collection of attributes.
  • defStyleAttr – An attribute in the current theme that contains a reference to a style resource that supplies default values for the view. Can be 0 to not look for defaults.
/** * Create a new number picker * * @param context the application environment. * @param attrs a collection of attributes. * @param defStyleAttr An attribute in the current theme that contains a * reference to a style resource that supplies default values for * the view. Can be 0 to not look for defaults. */
public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); }
Create a new number picker
Params:
  • context – the application environment.
  • attrs – a collection of attributes.
  • defStyleAttr – An attribute in the current theme that contains a reference to a style resource that supplies default values for the view. Can be 0 to not look for defaults.
  • defStyleRes – A resource identifier of a style resource that supplies default values for the view, used only if defStyleAttr is 0 or can not be found in the theme. Can be 0 to not look for defaults.
/** * Create a new number picker * * @param context the application environment. * @param attrs a collection of attributes. * @param defStyleAttr An attribute in the current theme that contains a * reference to a style resource that supplies default values for * the view. Can be 0 to not look for defaults. * @param defStyleRes A resource identifier of a style resource that * supplies default values for the view, used only if * defStyleAttr is 0 or can not be found in the theme. Can be 0 * to not look for defaults. */
public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); // process style attributes final TypedArray attributesArray = context.obtainStyledAttributes( attrs, R.styleable.NumberPicker, defStyleAttr, defStyleRes); final int layoutResId = attributesArray.getResourceId( R.styleable.NumberPicker_internalLayout, DEFAULT_LAYOUT_RESOURCE_ID); mHasSelectorWheel = (layoutResId != DEFAULT_LAYOUT_RESOURCE_ID); mHideWheelUntilFocused = attributesArray.getBoolean( R.styleable.NumberPicker_hideWheelUntilFocused, false); mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0); final Drawable selectionDivider = attributesArray.getDrawable( R.styleable.NumberPicker_selectionDivider); if (selectionDivider != null) { selectionDivider.setCallback(this); selectionDivider.setLayoutDirection(getLayoutDirection()); if (selectionDivider.isStateful()) { selectionDivider.setState(getDrawableState()); } } mSelectionDivider = selectionDivider; final int defSelectionDividerHeight = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT, getResources().getDisplayMetrics()); mSelectionDividerHeight = attributesArray.getDimensionPixelSize( R.styleable.NumberPicker_selectionDividerHeight, defSelectionDividerHeight); final int defSelectionDividerDistance = (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE, getResources().getDisplayMetrics()); mSelectionDividersDistance = attributesArray.getDimensionPixelSize( R.styleable.NumberPicker_selectionDividersDistance, defSelectionDividerDistance); mMinHeight = attributesArray.getDimensionPixelSize( R.styleable.NumberPicker_internalMinHeight, SIZE_UNSPECIFIED); mMaxHeight = attributesArray.getDimensionPixelSize( R.styleable.NumberPicker_internalMaxHeight, SIZE_UNSPECIFIED); if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED && mMinHeight > mMaxHeight) { throw new IllegalArgumentException("minHeight > maxHeight"); } mMinWidth = attributesArray.getDimensionPixelSize( R.styleable.NumberPicker_internalMinWidth, SIZE_UNSPECIFIED); mMaxWidth = attributesArray.getDimensionPixelSize( R.styleable.NumberPicker_internalMaxWidth, SIZE_UNSPECIFIED); if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED && mMinWidth > mMaxWidth) { throw new IllegalArgumentException("minWidth > maxWidth"); } mComputeMaxWidth = (mMaxWidth == SIZE_UNSPECIFIED); mVirtualButtonPressedDrawable = attributesArray.getDrawable( R.styleable.NumberPicker_virtualButtonPressedDrawable); attributesArray.recycle(); mPressedStateHelper = new PressedStateHelper(); // By default Linearlayout that we extend is not drawn. This is // its draw() method is not called but dispatchDraw() is called // directly (see ViewGroup.drawChild()). However, this class uses // the fading edge effect implemented by View and we need our // draw() method to be called. Therefore, we declare we will draw. setWillNotDraw(!mHasSelectorWheel); LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(layoutResId, this, true); OnClickListener onClickListener = new OnClickListener() { public void onClick(View v) { hideSoftInput(); mInputText.clearFocus(); if (v.getId() == R.id.increment) { changeValueByOne(true); } else { changeValueByOne(false); } } }; OnLongClickListener onLongClickListener = new OnLongClickListener() { public boolean onLongClick(View v) { hideSoftInput(); mInputText.clearFocus(); if (v.getId() == R.id.increment) { postChangeCurrentByOneFromLongPress(true, 0); } else { postChangeCurrentByOneFromLongPress(false, 0); } return true; } }; // increment button if (!mHasSelectorWheel) { mIncrementButton = findViewById(R.id.increment); mIncrementButton.setOnClickListener(onClickListener); mIncrementButton.setOnLongClickListener(onLongClickListener); } else { mIncrementButton = null; } // decrement button if (!mHasSelectorWheel) { mDecrementButton = findViewById(R.id.decrement); mDecrementButton.setOnClickListener(onClickListener); mDecrementButton.setOnLongClickListener(onLongClickListener); } else { mDecrementButton = null; } // input text mInputText = findViewById(R.id.numberpicker_input); mInputText.setOnFocusChangeListener(new OnFocusChangeListener() { public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { mInputText.selectAll(); } else { mInputText.setSelection(0, 0); validateInputTextView(v); } } }); mInputText.setFilters(new InputFilter[] { new InputTextFilter() }); mInputText.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); mInputText.setImeOptions(EditorInfo.IME_ACTION_DONE); // initialize constants ViewConfiguration configuration = ViewConfiguration.get(context); mTouchSlop = configuration.getScaledTouchSlop(); mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT; mTextSize = (int) mInputText.getTextSize(); // create the selector wheel paint Paint paint = new Paint(); paint.setAntiAlias(true); paint.setTextAlign(Align.CENTER); paint.setTextSize(mTextSize); paint.setTypeface(mInputText.getTypeface()); ColorStateList colors = mInputText.getTextColors(); int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE); paint.setColor(color); mSelectorWheelPaint = paint; // create the fling and adjust scrollers mFlingScroller = new Scroller(getContext(), null, true); mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f)); updateInputTextView(); // If not explicitly specified this view is important for accessibility. if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } // Should be focusable by default, as the text view whose visibility changes is focusable if (getFocusable() == View.FOCUSABLE_AUTO) { setFocusable(View.FOCUSABLE); setFocusableInTouchMode(true); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (!mHasSelectorWheel) { super.onLayout(changed, left, top, right, bottom); return; } final int msrdWdth = getMeasuredWidth(); final int msrdHght = getMeasuredHeight(); // Input text centered horizontally. final int inptTxtMsrdWdth = mInputText.getMeasuredWidth(); final int inptTxtMsrdHght = mInputText.getMeasuredHeight(); final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2; final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2; final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth; final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght; mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom); if (changed) { // need to do all this when we know our size initializeSelectorWheel(); initializeFadingEdges(); mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2 - mSelectionDividerHeight; mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight + mSelectionDividersDistance; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (!mHasSelectorWheel) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } // Try greedily to fit the max width and height. final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth); final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight); super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec); // Flag if we are measured with width or height less than the respective min. final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(), widthMeasureSpec); final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(), heightMeasureSpec); setMeasuredDimension(widthSize, heightSize); }
Move to the final position of a scroller. Ensures to force finish the scroller and if it is not at its final position a scroll of the selector wheel is performed to fast forward to the final position.
Params:
  • scroller – The scroller to whose final position to get.
Returns:True of the a move was performed, i.e. the scroller was not in final position.
/** * Move to the final position of a scroller. Ensures to force finish the scroller * and if it is not at its final position a scroll of the selector wheel is * performed to fast forward to the final position. * * @param scroller The scroller to whose final position to get. * @return True of the a move was performed, i.e. the scroller was not in final position. */
private boolean moveToFinalScrollerPosition(Scroller scroller) { scroller.forceFinished(true); int amountToScroll = scroller.getFinalY() - scroller.getCurrY(); int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementHeight; int overshootAdjustment = mInitialScrollOffset - futureScrollOffset; if (overshootAdjustment != 0) { if (Math.abs(overshootAdjustment) > mSelectorElementHeight / 2) { if (overshootAdjustment > 0) { overshootAdjustment -= mSelectorElementHeight; } else { overshootAdjustment += mSelectorElementHeight; } } amountToScroll += overshootAdjustment; scrollBy(0, amountToScroll); return true; } return false; } @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (!mHasSelectorWheel || !isEnabled()) { return false; } final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_DOWN: { removeAllCallbacks(); hideSoftInput(); mLastDownOrMoveEventY = mLastDownEventY = event.getY(); mLastDownEventTime = event.getEventTime(); mIgnoreMoveEvents = false; mPerformClickOnTap = false; // Handle pressed state before any state change. if (mLastDownEventY < mTopSelectionDividerTop) { if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { mPressedStateHelper.buttonPressDelayed( PressedStateHelper.BUTTON_DECREMENT); } } else if (mLastDownEventY > mBottomSelectionDividerBottom) { if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { mPressedStateHelper.buttonPressDelayed( PressedStateHelper.BUTTON_INCREMENT); } } // Make sure we support flinging inside scrollables. getParent().requestDisallowInterceptTouchEvent(true); if (!mFlingScroller.isFinished()) { mFlingScroller.forceFinished(true); mAdjustScroller.forceFinished(true); onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } else if (!mAdjustScroller.isFinished()) { mFlingScroller.forceFinished(true); mAdjustScroller.forceFinished(true); } else if (mLastDownEventY < mTopSelectionDividerTop) { postChangeCurrentByOneFromLongPress( false, ViewConfiguration.getLongPressTimeout()); } else if (mLastDownEventY > mBottomSelectionDividerBottom) { postChangeCurrentByOneFromLongPress( true, ViewConfiguration.getLongPressTimeout()); } else { mPerformClickOnTap = true; postBeginSoftInputOnLongPressCommand(); } return true; } } return false; } @Override public boolean onTouchEvent(MotionEvent event) { if (!isEnabled() || !mHasSelectorWheel) { return false; } if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_MOVE: { if (mIgnoreMoveEvents) { break; } float currentMoveY = event.getY(); if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); if (deltaDownY > mTouchSlop) { removeAllCallbacks(); onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } } else { int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY)); scrollBy(0, deltaMoveY); invalidate(); } mLastDownOrMoveEventY = currentMoveY; } break; case MotionEvent.ACTION_UP: { removeBeginSoftInputCommand(); removeChangeCurrentByOneFromLongPress(); mPressedStateHelper.cancel(); VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); int initialVelocity = (int) velocityTracker.getYVelocity(); if (Math.abs(initialVelocity) > mMinimumFlingVelocity) { fling(initialVelocity); onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); } else { int eventY = (int) event.getY(); int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY); long deltaTime = event.getEventTime() - mLastDownEventTime; if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) { if (mPerformClickOnTap) { mPerformClickOnTap = false; performClick(); } else { int selectorIndexOffset = (eventY / mSelectorElementHeight) - SELECTOR_MIDDLE_ITEM_INDEX; if (selectorIndexOffset > 0) { changeValueByOne(true); mPressedStateHelper.buttonTapped( PressedStateHelper.BUTTON_INCREMENT); } else if (selectorIndexOffset < 0) { changeValueByOne(false); mPressedStateHelper.buttonTapped( PressedStateHelper.BUTTON_DECREMENT); } } } else { ensureScrollWheelAdjusted(); } onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } mVelocityTracker.recycle(); mVelocityTracker = null; } break; } return true; } @Override public boolean dispatchTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: removeAllCallbacks(); break; } return super.dispatchTouchEvent(event); } @Override public boolean dispatchKeyEvent(KeyEvent event) { final int keyCode = event.getKeyCode(); switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: removeAllCallbacks(); break; case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_UP: if (!mHasSelectorWheel) { break; } switch (event.getAction()) { case KeyEvent.ACTION_DOWN: if (mWrapSelectorWheel || ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN) ? getValue() < getMaxValue() : getValue() > getMinValue())) { requestFocus(); mLastHandledDownDpadKeyCode = keyCode; removeAllCallbacks(); if (mFlingScroller.isFinished()) { changeValueByOne(keyCode == KeyEvent.KEYCODE_DPAD_DOWN); } return true; } break; case KeyEvent.ACTION_UP: if (mLastHandledDownDpadKeyCode == keyCode) { mLastHandledDownDpadKeyCode = -1; return true; } break; } } return super.dispatchKeyEvent(event); } @Override public boolean dispatchTrackballEvent(MotionEvent event) { final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: removeAllCallbacks(); break; } return super.dispatchTrackballEvent(event); } @Override protected boolean dispatchHoverEvent(MotionEvent event) { if (!mHasSelectorWheel) { return super.dispatchHoverEvent(event); } if (AccessibilityManager.getInstance(mContext).isEnabled()) { final int eventY = (int) event.getY(); final int hoveredVirtualViewId; if (eventY < mTopSelectionDividerTop) { hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_DECREMENT; } else if (eventY > mBottomSelectionDividerBottom) { hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INCREMENT; } else { hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT; } final int action = event.getActionMasked(); AccessibilityNodeProviderImpl provider = (AccessibilityNodeProviderImpl) getAccessibilityNodeProvider(); switch (action) { case MotionEvent.ACTION_HOVER_ENTER: { provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); mLastHoveredChildVirtualViewId = hoveredVirtualViewId; provider.performAction(hoveredVirtualViewId, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); } break; case MotionEvent.ACTION_HOVER_MOVE: { if (mLastHoveredChildVirtualViewId != hoveredVirtualViewId && mLastHoveredChildVirtualViewId != View.NO_ID) { provider.sendAccessibilityEventForVirtualView( mLastHoveredChildVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); mLastHoveredChildVirtualViewId = hoveredVirtualViewId; provider.performAction(hoveredVirtualViewId, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); } } break; case MotionEvent.ACTION_HOVER_EXIT: { provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); mLastHoveredChildVirtualViewId = View.NO_ID; } break; } } return false; } @Override public void computeScroll() { Scroller scroller = mFlingScroller; if (scroller.isFinished()) { scroller = mAdjustScroller; if (scroller.isFinished()) { return; } } scroller.computeScrollOffset(); int currentScrollerY = scroller.getCurrY(); if (mPreviousScrollerY == 0) { mPreviousScrollerY = scroller.getStartY(); } scrollBy(0, currentScrollerY - mPreviousScrollerY); mPreviousScrollerY = currentScrollerY; if (scroller.isFinished()) { onScrollerFinished(scroller); } else { invalidate(); } } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); if (!mHasSelectorWheel) { mIncrementButton.setEnabled(enabled); } if (!mHasSelectorWheel) { mDecrementButton.setEnabled(enabled); } mInputText.setEnabled(enabled); } @Override public void scrollBy(int x, int y) { int[] selectorIndices = mSelectorIndices; int startScrollOffset = mCurrentScrollOffset; if (!mWrapSelectorWheel && y > 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { mCurrentScrollOffset = mInitialScrollOffset; return; } if (!mWrapSelectorWheel && y < 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { mCurrentScrollOffset = mInitialScrollOffset; return; } mCurrentScrollOffset += y; while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) { mCurrentScrollOffset -= mSelectorElementHeight; decrementSelectorIndices(selectorIndices); setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { mCurrentScrollOffset = mInitialScrollOffset; } } while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) { mCurrentScrollOffset += mSelectorElementHeight; incrementSelectorIndices(selectorIndices); setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { mCurrentScrollOffset = mInitialScrollOffset; } } if (startScrollOffset != mCurrentScrollOffset) { onScrollChanged(0, mCurrentScrollOffset, 0, startScrollOffset); } } @Override protected int computeVerticalScrollOffset() { return mCurrentScrollOffset; } @Override protected int computeVerticalScrollRange() { return (mMaxValue - mMinValue + 1) * mSelectorElementHeight; } @Override protected int computeVerticalScrollExtent() { return getHeight(); } @Override public int getSolidColor() { return mSolidColor; }
Sets the listener to be notified on change of the current value.
Params:
  • onValueChangedListener – The listener.
/** * Sets the listener to be notified on change of the current value. * * @param onValueChangedListener The listener. */
public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) { mOnValueChangeListener = onValueChangedListener; }
Set listener to be notified for scroll state changes.
Params:
  • onScrollListener – The listener.
/** * Set listener to be notified for scroll state changes. * * @param onScrollListener The listener. */
public void setOnScrollListener(OnScrollListener onScrollListener) { mOnScrollListener = onScrollListener; }
Set the formatter to be used for formatting the current value.

Note: If you have provided alternative values for the values this formatter is never invoked.

Params:
See Also:
/** * Set the formatter to be used for formatting the current value. * <p> * Note: If you have provided alternative values for the values this * formatter is never invoked. * </p> * * @param formatter The formatter object. If formatter is <code>null</code>, * {@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 getMinValue() and getWrapSelectorWheel() is false the current value is set to the getMinValue() value.

If the argument is less than the getMinValue() and getWrapSelectorWheel() is true the current value is set to the getMaxValue() value.

If the argument is less than the getMaxValue() and getWrapSelectorWheel() is false the current value is set to the getMaxValue() value.

If the argument is less than the getMaxValue() and getWrapSelectorWheel() is true the current value is set to the getMinValue() value.

Params:
  • value – The current value.
See Also:
/** * Set the current value for the number picker. * <p> * If the argument is less than the {@link NumberPicker#getMinValue()} and * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the * current value is set to the {@link NumberPicker#getMinValue()} value. * </p> * <p> * If the argument is less than the {@link NumberPicker#getMinValue()} and * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the * current value is set to the {@link NumberPicker#getMaxValue()} value. * </p> * <p> * If the argument is less than the {@link NumberPicker#getMaxValue()} and * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the * current value is set to the {@link NumberPicker#getMaxValue()} value. * </p> * <p> * If the argument is less than the {@link NumberPicker#getMaxValue()} and * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the * current value is set to the {@link NumberPicker#getMinValue()} value. * </p> * * @param value The current value. * @see #setWrapSelectorWheel(boolean) * @see #setMinValue(int) * @see #setMaxValue(int) */
public void setValue(int value) { setValueInternal(value, false); } @Override public boolean performClick() { if (!mHasSelectorWheel) { return super.performClick(); } else if (!super.performClick()) { showSoftInput(); } return true; } @Override public boolean performLongClick() { if (!mHasSelectorWheel) { return super.performLongClick(); } else if (!super.performLongClick()) { showSoftInput(); mIgnoreMoveEvents = true; } return true; }
Shows the soft input for its input text.
/** * Shows the soft input for its input text. */
private void showSoftInput() { InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); if (inputMethodManager != null) { if (mHasSelectorWheel) { mInputText.setVisibility(View.VISIBLE); } mInputText.requestFocus(); inputMethodManager.showSoftInput(mInputText, 0); } }
Hides the soft input if it is active for the input text.
/** * Hides the soft input if it is active for the input text. */
private void hideSoftInput() { InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) { inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); } if (mHasSelectorWheel) { mInputText.setVisibility(View.INVISIBLE); } }
Computes the max width if no such specified as an attribute.
/** * Computes the max width if no such specified as an attribute. */
private void tryComputeMaxWidth() { if (!mComputeMaxWidth) { return; } int maxTextWidth = 0; if (mDisplayedValues == null) { float maxDigitWidth = 0; for (int i = 0; i <= 9; i++) { final float digitWidth = mSelectorWheelPaint.measureText(formatNumberWithLocale(i)); if (digitWidth > maxDigitWidth) { maxDigitWidth = digitWidth; } } int numberOfDigits = 0; int current = mMaxValue; while (current > 0) { numberOfDigits++; current = current / 10; } maxTextWidth = (int) (numberOfDigits * maxDigitWidth); } else { final int valueCount = mDisplayedValues.length; for (int i = 0; i < valueCount; i++) { final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]); if (textWidth > maxTextWidth) { maxTextWidth = (int) textWidth; } } } maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight(); if (mMaxWidth != maxTextWidth) { if (maxTextWidth > mMinWidth) { mMaxWidth = maxTextWidth; } else { mMaxWidth = mMinWidth; } invalidate(); } }
Gets whether the selector wheel wraps when reaching the min/max value.
See Also:
Returns:True if the selector wheel wraps.
/** * Gets whether the selector wheel wraps when reaching the min/max value. * * @return True if the selector wheel wraps. * * @see #getMinValue() * @see #getMaxValue() */
public boolean getWrapSelectorWheel() { return mWrapSelectorWheel; }
Sets whether the selector wheel shown during flinging/scrolling should wrap around the getMinValue() and getMaxValue() values.

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 ( getMaxValue() - 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.

Params:
  • wrapSelectorWheel – Whether to wrap.
/** * Sets whether the selector wheel shown during flinging/scrolling should * wrap around the {@link NumberPicker#getMinValue()} and * {@link NumberPicker#getMaxValue()} values. * <p> * 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. * </p> * <p> * <strong>Note:</strong> 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. * </p> * * @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 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.
/** * 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.

Params:
  • intervalMillis – The speed (in milliseconds) at which the numbers will be incremented and decremented.
/** * Sets the speed at which the numbers be incremented and decremented when * the up and down buttons are long pressed respectively. * <p> * The default value is 300 ms. * </p> * * @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.
Returns:The value.
/** * Returns the value of the picker. * * @return The value. */
public int getValue() { return mValue; }
Returns the min value of the picker.
Returns:The min value
/** * Returns the min value of the picker. * * @return The min value */
public int getMinValue() { return mMinValue; }
Sets the min value of the picker.
Params:
/** * Sets the min value of the picker. * * @param minValue The min value inclusive. * * <strong>Note:</strong> 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.
Returns:The max value.
/** * Returns the max value of the picker. * * @return The max value. */
public int getMaxValue() { return mMaxValue; }
Sets the max value of the picker.
Params:
/** * Sets the max value of the picker. * * @param maxValue The max value inclusive. * * <strong>Note:</strong> 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.
Returns:The displayed values.
/** * 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.
Params:
  • 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 getMaxValue() - getMinValue() + 1.
/** * Sets the values to be displayed. * * @param displayedValues The displayed values. * * <strong>Note:</strong> 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
/** * 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
/** @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
/** @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.
Params:
  • measureSpec – The measure spec.
  • maxSize – The max value for the size.
Returns:A measure spec greedily imposing the max size.
/** * 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.
Params:
  • minSize – The minimal desired size.
  • measuredSize – The currently measured size.
  • measureSpec – The current measure spec.
Returns:The resolved size and state.
/** * 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.
/** * 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.
Params:
  • current – The new value of the NumberPicker.
  • notifyChange – Whether to notify if the current value changed.
/** * 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.
Params:
  • increment – True to increment, false to decrement.
/** * 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 given scroller.
/** * Callback invoked upon completion of a given <code>scroller</code>. */
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
/** * Handles transition to a given <code>scrollState</code> */
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.
/** * Flings the selector with the given <code>velocityY</code>. */
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(); }
Returns:The wrapped index selectorIndex value.
/** * @return The wrapped index <code>selectorIndex</code> 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.
/** * Increments the <code>selectorIndices</code> 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.
/** * Decrements the <code>selectorIndices</code> 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.
/** * Ensures we have a cached string representation of the given <code> * selectorIndex</code> to avoid multiple instantiations of the same string. */
private void ensureCachedScrollSelectorValue(int selectorIndex) { SparseArray<String> cache = mSelectorIndexToStringCache; String scrollSelectorValue = cache.get(selectorIndex); if (scrollSelectorValue != null) { return; } if (selectorIndex < mMinValue || selectorIndex > mMaxValue) { scrollSelectorValue = ""; } else { if (mDisplayedValues != null) { int displayedValueIndex = selectorIndex - mMinValue; scrollSelectorValue = mDisplayedValues[displayedValueIndex]; } else { scrollSelectorValue = formatNumber(selectorIndex); } } cache.put(selectorIndex, scrollSelectorValue); } private String formatNumber(int value) { return (mFormatter != null) ? mFormatter.format(value) : formatNumberWithLocale(value); } private void validateInputTextView(View v) { String str = String.valueOf(((TextView) v).getText()); if (TextUtils.isEmpty(str)) { // Restore to the old value as we don't allow empty values updateInputTextView(); } else { // Check the new value and ensure it's in range int current = getSelectedPos(str.toString()); setValueInternal(current, true); } }
Updates the view of this NumberPicker. If displayValues were specified in the string corresponding to the index specified by the current value will be returned. Otherwise, the formatter specified in setFormatter will be used to format the number.
Returns:Whether the text was updated.
/** * Updates the view of this NumberPicker. If displayValues were specified in * 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. * * @return Whether the text was updated. */
private boolean updateInputTextView() { /* * 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. */ String text = (mDisplayedValues == null) ? formatNumber(mValue) : mDisplayedValues[mValue - mMinValue]; if (!TextUtils.isEmpty(text)) { CharSequence beforeText = mInputText.getText(); if (!text.equals(beforeText.toString())) { mInputText.setText(text); if (AccessibilityManager.getInstance(mContext).isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain( AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); mInputText.onInitializeAccessibilityEvent(event); mInputText.onPopulateAccessibilityEvent(event); event.setFromIndex(0); event.setRemovedCount(beforeText.length()); event.setAddedCount(text.length()); event.setBeforeText(beforeText); event.setSource(NumberPicker.this, AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT); requestSendAccessibilityEvent(NumberPicker.this, event); } return true; } } return false; }
Notifies the listener, if registered, of a change of the value of this NumberPicker.
/** * Notifies the listener, if registered, of a change of the value of this * NumberPicker. */
private void notifyChange(int previous, int current) { if (mOnValueChangeListener != null) { mOnValueChangeListener.onValueChange(this, previous, mValue); } }
Posts a command for changing the current value by one.
Params:
  • increment – Whether to increment or decrement the value.
/** * Posts a command for changing the current value by one. * * @param increment Whether to increment or decrement the value. */
private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) { if (mChangeCurrentByOneFromLongPressCommand == null) { mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand(); } else { removeCallbacks(mChangeCurrentByOneFromLongPressCommand); } mChangeCurrentByOneFromLongPressCommand.setStep(increment); postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis); }
Removes the command for changing the current value by one.
/** * Removes the command for changing the current value by one. */
private void removeChangeCurrentByOneFromLongPress() { if (mChangeCurrentByOneFromLongPressCommand != null) { removeCallbacks(mChangeCurrentByOneFromLongPressCommand); } }
Posts a command for beginning an edit of the current value via IME on long press.
/** * Posts a command for beginning an edit of the current value via IME on * long press. */
private void postBeginSoftInputOnLongPressCommand() { if (mBeginSoftInputOnLongPressCommand == null) { mBeginSoftInputOnLongPressCommand = new BeginSoftInputOnLongPressCommand(); } else { removeCallbacks(mBeginSoftInputOnLongPressCommand); } postDelayed(mBeginSoftInputOnLongPressCommand, ViewConfiguration.getLongPressTimeout()); }
Removes the command for beginning an edit of the current value via IME.
/** * Removes the command for beginning an edit of the current value via IME. */
private void removeBeginSoftInputCommand() { if (mBeginSoftInputOnLongPressCommand != null) { removeCallbacks(mBeginSoftInputOnLongPressCommand); } }
Removes all pending callback from the message queue.
/** * Removes all pending callback from the message queue. */
private void removeAllCallbacks() { if (mChangeCurrentByOneFromLongPressCommand != null) { removeCallbacks(mChangeCurrentByOneFromLongPressCommand); } if (mSetSelectionCommand != null) { mSetSelectionCommand.cancel(); } if (mBeginSoftInputOnLongPressCommand != null) { removeCallbacks(mBeginSoftInputOnLongPressCommand); } mPressedStateHelper.cancel(); }
Returns:The selected index given its displayed value.
/** * @return The selected index given its displayed <code>value</code>. */
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 SetSelectionCommand from the given selectionStart to selectionEnd.
/** * 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 Filter
/** * 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.
/** * 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.
Returns:Whether an adjustment has been made.
/** * 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.
/** * 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.
/** 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.
/** * 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
/** * @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.
/** * 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 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<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched, int virtualViewId) { if (TextUtils.isEmpty(searched)) { return Collections.emptyList(); } String searchedLowerCase = searched.toLowerCase(); List<AccessibilityNodeInfo> result = new ArrayList<AccessibilityNodeInfo>(); switch (virtualViewId) { case View.NO_ID: { findAccessibilityNodeInfosByTextInChild(searchedLowerCase, VIRTUAL_VIEW_ID_DECREMENT, result); findAccessibilityNodeInfosByTextInChild(searchedLowerCase, VIRTUAL_VIEW_ID_INPUT, result); findAccessibilityNodeInfosByTextInChild(searchedLowerCase, VIRTUAL_VIEW_ID_INCREMENT, result); return result; } case VIRTUAL_VIEW_ID_DECREMENT: case VIRTUAL_VIEW_ID_INCREMENT: case VIRTUAL_VIEW_ID_INPUT: { findAccessibilityNodeInfosByTextInChild(searchedLowerCase, virtualViewId, result); return result; } } return super.findAccessibilityNodeInfosByText(searched, virtualViewId); } @Override public boolean performAction(int virtualViewId, int action, Bundle arguments) { switch (virtualViewId) { case View.NO_ID: { switch (action) { case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { if (mAccessibilityFocusedView != virtualViewId) { mAccessibilityFocusedView = virtualViewId; requestAccessibilityFocus(); return true; } } return false; case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { if (mAccessibilityFocusedView == virtualViewId) { mAccessibilityFocusedView = UNDEFINED; clearAccessibilityFocus(); return true; } return false; } case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { if (NumberPicker.this.isEnabled() && (getWrapSelectorWheel() || getValue() < getMaxValue())) { changeValueByOne(true); return true; } } return false; case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { if (NumberPicker.this.isEnabled() && (getWrapSelectorWheel() || getValue() > getMinValue())) { changeValueByOne(false); return true; } } return false; } } break; case VIRTUAL_VIEW_ID_INPUT: { switch (action) { case AccessibilityNodeInfo.ACTION_FOCUS: { if (NumberPicker.this.isEnabled() && !mInputText.isFocused()) { return mInputText.requestFocus(); } } break; case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: { if (NumberPicker.this.isEnabled() && mInputText.isFocused()) { mInputText.clearFocus(); return true; } return false; } case AccessibilityNodeInfo.ACTION_CLICK: { if (NumberPicker.this.isEnabled()) { performClick(); return true; } return false; } case AccessibilityNodeInfo.ACTION_LONG_CLICK: { if (NumberPicker.this.isEnabled()) { performLongClick(); return true; } return false; } case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { if (mAccessibilityFocusedView != virtualViewId) { mAccessibilityFocusedView = virtualViewId; sendAccessibilityEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); mInputText.invalidate(); return true; } } return false; case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { if (mAccessibilityFocusedView == virtualViewId) { mAccessibilityFocusedView = UNDEFINED; sendAccessibilityEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); mInputText.invalidate(); return true; } } return false; default: { return mInputText.performAccessibilityAction(action, arguments); } } } return false; case VIRTUAL_VIEW_ID_INCREMENT: { switch (action) { case AccessibilityNodeInfo.ACTION_CLICK: { if (NumberPicker.this.isEnabled()) { NumberPicker.this.changeValueByOne(true); sendAccessibilityEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED); return true; } } return false; case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { if (mAccessibilityFocusedView != virtualViewId) { mAccessibilityFocusedView = virtualViewId; sendAccessibilityEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); return true; } } return false; case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { if (mAccessibilityFocusedView == virtualViewId) { mAccessibilityFocusedView = UNDEFINED; sendAccessibilityEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); return true; } } return false; } } return false; case VIRTUAL_VIEW_ID_DECREMENT: { switch (action) { case AccessibilityNodeInfo.ACTION_CLICK: { if (NumberPicker.this.isEnabled()) { final boolean increment = (virtualViewId == VIRTUAL_VIEW_ID_INCREMENT); NumberPicker.this.changeValueByOne(increment); sendAccessibilityEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_CLICKED); return true; } } return false; case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: { if (mAccessibilityFocusedView != virtualViewId) { mAccessibilityFocusedView = virtualViewId; sendAccessibilityEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); invalidate(0, 0, mRight, mTopSelectionDividerTop); return true; } } return false; case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: { if (mAccessibilityFocusedView == virtualViewId) { mAccessibilityFocusedView = UNDEFINED; sendAccessibilityEventForVirtualView(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); invalidate(0, 0, mRight, mTopSelectionDividerTop); return true; } } return false; } } return false; } return super.performAction(virtualViewId, action, arguments); } public void sendAccessibilityEventForVirtualView(int virtualViewId, int eventType) { switch (virtualViewId) { case VIRTUAL_VIEW_ID_DECREMENT: { if (hasVirtualDecrementButton()) { sendAccessibilityEventForVirtualButton(virtualViewId, eventType, getVirtualDecrementButtonText()); } } break; case VIRTUAL_VIEW_ID_INPUT: { sendAccessibilityEventForVirtualText(eventType); } break; case VIRTUAL_VIEW_ID_INCREMENT: { if (hasVirtualIncrementButton()) { sendAccessibilityEventForVirtualButton(virtualViewId, eventType, getVirtualIncrementButtonText()); } } break; } } private void sendAccessibilityEventForVirtualText(int eventType) { if (AccessibilityManager.getInstance(mContext).isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain(eventType); mInputText.onInitializeAccessibilityEvent(event); mInputText.onPopulateAccessibilityEvent(event); event.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT); requestSendAccessibilityEvent(NumberPicker.this, event); } } private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType, String text) { if (AccessibilityManager.getInstance(mContext).isEnabled()) { AccessibilityEvent event = AccessibilityEvent.obtain(eventType); event.setClassName(Button.class.getName()); event.setPackageName(mContext.getPackageName()); event.getText().add(text); event.setEnabled(NumberPicker.this.isEnabled()); event.setSource(NumberPicker.this, virtualViewId); requestSendAccessibilityEvent(NumberPicker.this, event); } } private void findAccessibilityNodeInfosByTextInChild(String searchedLowerCase, int virtualViewId, List<AccessibilityNodeInfo> outResult) { switch (virtualViewId) { case VIRTUAL_VIEW_ID_DECREMENT: { String text = getVirtualDecrementButtonText(); if (!TextUtils.isEmpty(text) && text.toString().toLowerCase().contains(searchedLowerCase)) { outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT)); } } return; case VIRTUAL_VIEW_ID_INPUT: { CharSequence text = mInputText.getText(); if (!TextUtils.isEmpty(text) && text.toString().toLowerCase().contains(searchedLowerCase)) { outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT)); return; } CharSequence contentDesc = mInputText.getText(); if (!TextUtils.isEmpty(contentDesc) && contentDesc.toString().toLowerCase().contains(searchedLowerCase)) { outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT)); return; } } break; case VIRTUAL_VIEW_ID_INCREMENT: { String text = getVirtualIncrementButtonText(); if (!TextUtils.isEmpty(text) && text.toString().toLowerCase().contains(searchedLowerCase)) { outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT)); } } return; } } private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText( int left, int top, int right, int bottom) { AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo(); info.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT); if (mAccessibilityFocusedView != VIRTUAL_VIEW_ID_INPUT) { info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); } if (mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT) { info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); } Rect boundsInParent = mTempRect; boundsInParent.set(left, top, right, bottom); info.setVisibleToUser(isVisibleToUser(boundsInParent)); info.setBoundsInParent(boundsInParent); Rect boundsInScreen = boundsInParent; int[] locationOnScreen = mTempArray; getLocationOnScreen(locationOnScreen); boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); info.setBoundsInScreen(boundsInScreen); return info; } private AccessibilityNodeInfo createAccessibilityNodeInfoForVirtualButton(int virtualViewId, String text, int left, int top, int right, int bottom) { AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); info.setClassName(Button.class.getName()); info.setPackageName(mContext.getPackageName()); info.setSource(NumberPicker.this, virtualViewId); info.setParent(NumberPicker.this); info.setText(text); info.setClickable(true); info.setLongClickable(true); info.setEnabled(NumberPicker.this.isEnabled()); Rect boundsInParent = mTempRect; boundsInParent.set(left, top, right, bottom); info.setVisibleToUser(isVisibleToUser(boundsInParent)); info.setBoundsInParent(boundsInParent); Rect boundsInScreen = boundsInParent; int[] locationOnScreen = mTempArray; getLocationOnScreen(locationOnScreen); boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); info.setBoundsInScreen(boundsInScreen); if (mAccessibilityFocusedView != virtualViewId) { info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); } if (mAccessibilityFocusedView == virtualViewId) { info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); } if (NumberPicker.this.isEnabled()) { info.addAction(AccessibilityNodeInfo.ACTION_CLICK); } return info; } private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top, int right, int bottom) { AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(); info.setClassName(NumberPicker.class.getName()); info.setPackageName(mContext.getPackageName()); info.setSource(NumberPicker.this); if (hasVirtualDecrementButton()) { info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_DECREMENT); } info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT); if (hasVirtualIncrementButton()) { info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INCREMENT); } info.setParent((View) getParentForAccessibility()); info.setEnabled(NumberPicker.this.isEnabled()); info.setScrollable(true); final float applicationScale = getContext().getResources().getCompatibilityInfo().applicationScale; Rect boundsInParent = mTempRect; boundsInParent.set(left, top, right, bottom); boundsInParent.scale(applicationScale); info.setBoundsInParent(boundsInParent); info.setVisibleToUser(isVisibleToUser()); Rect boundsInScreen = boundsInParent; int[] locationOnScreen = mTempArray; getLocationOnScreen(locationOnScreen); boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]); boundsInScreen.scale(applicationScale); info.setBoundsInScreen(boundsInScreen); if (mAccessibilityFocusedView != View.NO_ID) { info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS); } if (mAccessibilityFocusedView == View.NO_ID) { info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS); } if (NumberPicker.this.isEnabled()) { if (getWrapSelectorWheel() || getValue() < getMaxValue()) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); } if (getWrapSelectorWheel() || getValue() > getMinValue()) { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } } return info; } private boolean hasVirtualDecrementButton() { return getWrapSelectorWheel() || getValue() > getMinValue(); } private boolean hasVirtualIncrementButton() { return getWrapSelectorWheel() || getValue() < getMaxValue(); } private String getVirtualDecrementButtonText() { int value = mValue - 1; if (mWrapSelectorWheel) { value = getWrappedSelectorIndex(value); } if (value >= mMinValue) { return (mDisplayedValues == null) ? formatNumber(value) : mDisplayedValues[value - mMinValue]; } return null; } private String getVirtualIncrementButtonText() { int value = mValue + 1; if (mWrapSelectorWheel) { value = getWrappedSelectorIndex(value); } if (value <= mMaxValue) { return (mDisplayedValues == null) ? formatNumber(value) : mDisplayedValues[value - mMinValue]; } return null; } } static private String formatNumberWithLocale(int value) { return String.format(Locale.getDefault(), "%d", value); } }