| /* |
| * 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.ColorInt; |
| import android.annotation.FloatRange; |
| import android.annotation.IntDef; |
| import android.annotation.IntRange; |
| import android.annotation.Px; |
| import android.annotation.TestApi; |
| import android.annotation.Widget; |
| import android.compat.annotation.UnsupportedAppUsage; |
| 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.icu.text.DecimalFormatSymbols; |
| import android.os.Build; |
| 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 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. |
| * <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> |
| * <li> |
| * If the current theme is derived from {@link android.R.style#Theme_Material} |
| * the widget presents the current value as a scrolling vertical selector with |
| * the selected value in the center and the previous and following numbers above |
| * and below, separated by a divider. The value is changed by flinging vertically. |
| * The thickness of the divider can be changed by using the |
| * {@link android.R.attr#selectionDividerHeight} attribute and the color of the |
| * divider can be changed by using the |
| * {@link android.R.attr#colorControlNormal} attribute. |
| * </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. |
| */ |
| @UnsupportedAppUsage |
| private static final int SELECTOR_WHEEL_ITEM_COUNT = 3; |
| |
| /** |
| * The default update interval during long press. |
| */ |
| private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300; |
| |
| /** |
| * The index of the middle selector item. |
| */ |
| @UnsupportedAppUsage |
| private static final int SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2; |
| |
| /** |
| * The coefficient by which to adjust (divide) the max fling velocity. |
| */ |
| private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8; |
| |
| /** |
| * The the duration for adjusting the selector wheel. |
| */ |
| private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; |
| |
| /** |
| * The duration of scrolling while snapping to a given position. |
| */ |
| private static final int SNAP_SCROLL_DURATION = 300; |
| |
| /** |
| * The strength of fading in the top and bottom while drawing the selector. |
| */ |
| private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f; |
| |
| /** |
| * The default unscaled height of the selection divider. |
| */ |
| private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2; |
| |
| /** |
| * The default unscaled distance between the selection dividers. |
| */ |
| private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48; |
| |
| /** |
| * The resource id for the default layout. |
| */ |
| private static final int DEFAULT_LAYOUT_RESOURCE_ID = R.layout.number_picker; |
| |
| /** |
| * Constant for unspecified size. |
| */ |
| private static final int SIZE_UNSPECIFIED = -1; |
| |
| /** |
| * User choice on whether the selector wheel should be wrapped. |
| */ |
| private boolean mWrapSelectorWheelPreferred = true; |
| |
| /** |
| * Use a custom NumberPicker formatting callback to use two-digit minutes |
| * strings like "01". Keeping a static formatter etc. is the most efficient |
| * way to do this; it avoids creating temporary objects on every call to |
| * format(). |
| */ |
| private static class TwoDigitFormatter implements NumberPicker.Formatter { |
| final StringBuilder mBuilder = new StringBuilder(); |
| |
| char mZeroDigit; |
| java.util.Formatter mFmt; |
| |
| final Object[] mArgs = new Object[1]; |
| |
| TwoDigitFormatter() { |
| final Locale locale = Locale.getDefault(); |
| init(locale); |
| } |
| |
| private void init(Locale locale) { |
| mFmt = createFormatter(locale); |
| mZeroDigit = getZeroDigit(locale); |
| } |
| |
| public String format(int value) { |
| final Locale currentLocale = Locale.getDefault(); |
| if (mZeroDigit != getZeroDigit(currentLocale)) { |
| init(currentLocale); |
| } |
| mArgs[0] = value; |
| mBuilder.delete(0, mBuilder.length()); |
| mFmt.format("%02d", mArgs); |
| return mFmt.toString(); |
| } |
| |
| private static char getZeroDigit(Locale locale) { |
| return DecimalFormatSymbols.getInstance(locale).getZeroDigit(); |
| } |
| |
| private java.util.Formatter createFormatter(Locale locale) { |
| return new java.util.Formatter(mBuilder, locale); |
| } |
| } |
| |
| private static final TwoDigitFormatter sTwoDigitFormatter = new TwoDigitFormatter(); |
| |
| /** |
| * @hide |
| */ |
| @UnsupportedAppUsage |
| public static final Formatter getTwoDigitFormatter() { |
| return sTwoDigitFormatter; |
| } |
| |
| /** |
| * The increment button. |
| */ |
| private final ImageButton mIncrementButton; |
| |
| /** |
| * The decrement button. |
| */ |
| private final ImageButton mDecrementButton; |
| |
| /** |
| * The text for showing the current value. |
| */ |
| @UnsupportedAppUsage |
| private final EditText mInputText; |
| |
| /** |
| * The distance between the two selection dividers. |
| */ |
| private final int mSelectionDividersDistance; |
| |
| /** |
| * The min height of this widget. |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| private final int mMinHeight; |
| |
| /** |
| * The max height of this widget. |
| */ |
| private final int mMaxHeight; |
| |
| /** |
| * The max width of this widget. |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| private final int mMinWidth; |
| |
| /** |
| * The max width of this widget. |
| */ |
| private int mMaxWidth; |
| |
| /** |
| * Flag whether to compute the max width. |
| */ |
| private final boolean mComputeMaxWidth; |
| |
| /** |
| * The height of the text. |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| private final int mTextSize; |
| |
| /** |
| * The height of the gap between text elements if the selector wheel. |
| */ |
| private int mSelectorTextGapHeight; |
| |
| /** |
| * The values to be displayed instead the indices. |
| */ |
| private String[] mDisplayedValues; |
| |
| /** |
| * Lower value of the range of numbers allowed for the NumberPicker |
| */ |
| private int mMinValue; |
| |
| /** |
| * Upper value of the range of numbers allowed for the NumberPicker |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| private int mMaxValue; |
| |
| /** |
| * Current value of this NumberPicker |
| */ |
| private int mValue; |
| |
| /** |
| * Listener to be notified upon current value change. |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| private OnValueChangeListener mOnValueChangeListener; |
| |
| /** |
| * Listener to be notified upon scroll state change. |
| */ |
| private OnScrollListener mOnScrollListener; |
| |
| /** |
| * Formatter for for displaying the current value. |
| */ |
| private Formatter mFormatter; |
| |
| /** |
| * The speed for updating the value form long press. |
| */ |
| private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL; |
| |
| /** |
| * Cache for the string representation of selector indices. |
| */ |
| private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>(); |
| |
| /** |
| * The selector indices whose value are show by the selector. |
| */ |
| @UnsupportedAppUsage |
| private final int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT]; |
| |
| /** |
| * The {@link Paint} for drawing the selector. |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) |
| private final Paint mSelectorWheelPaint; |
| |
| /** |
| * The {@link Drawable} for pressed virtual (increment/decrement) buttons. |
| */ |
| private final Drawable mVirtualButtonPressedDrawable; |
| |
| /** |
| * The height of a selector element (text + gap). |
| */ |
| private int mSelectorElementHeight; |
| |
| /** |
| * The initial offset of the scroll selector. |
| */ |
| private int mInitialScrollOffset = Integer.MIN_VALUE; |
| |
| /** |
| * The current offset of the scroll selector. |
| */ |
| private int mCurrentScrollOffset; |
| |
| /** |
| * The {@link Scroller} responsible for flinging the selector. |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| private final Scroller mFlingScroller; |
| |
| /** |
| * The {@link Scroller} responsible for adjusting the selector. |
| */ |
| private final Scroller mAdjustScroller; |
| |
| /** |
| * The previous Y coordinate while scrolling the selector. |
| */ |
| private int mPreviousScrollerY; |
| |
| /** |
| * 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. |
| */ |
| private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand; |
| |
| /** |
| * 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. |
| */ |
| private float mLastDownEventY; |
| |
| /** |
| * The time of the last down event. |
| */ |
| private long mLastDownEventTime; |
| |
| /** |
| * The Y position of the last down or move event. |
| */ |
| private float mLastDownOrMoveEventY; |
| |
| /** |
| * Determines speed during touch scrolling. |
| */ |
| private VelocityTracker mVelocityTracker; |
| |
| /** |
| * @see ViewConfiguration#getScaledTouchSlop() |
| */ |
| private int mTouchSlop; |
| |
| /** |
| * @see ViewConfiguration#getScaledMinimumFlingVelocity() |
| */ |
| private int mMinimumFlingVelocity; |
| |
| /** |
| * @see ViewConfiguration#getScaledMaximumFlingVelocity() |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| private int mMaximumFlingVelocity; |
| |
| /** |
| * Flag whether the selector should wrap around. |
| */ |
| private boolean mWrapSelectorWheel; |
| |
| /** |
| * The back ground color used to optimize scroller fading. |
| */ |
| private final int mSolidColor; |
| |
| /** |
| * Flag whether this widget has a selector wheel. |
| */ |
| private final boolean mHasSelectorWheel; |
| |
| /** |
| * Divider for showing item to be selected while scrolling |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) |
| private final Drawable mSelectionDivider; |
| |
| /** |
| * The height of the selection divider. |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) |
| private int mSelectionDividerHeight; |
| |
| /** |
| * 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. |
| */ |
| private boolean mIgnoreMoveEvents; |
| |
| /** |
| * Flag whether to perform a click on tap. |
| */ |
| private boolean mPerformClickOnTap; |
| |
| /** |
| * The top of the top selection divider. |
| */ |
| private int mTopSelectionDividerTop; |
| |
| /** |
| * The bottom of the bottom selection divider. |
| */ |
| private int mBottomSelectionDividerBottom; |
| |
| /** |
| * The virtual id of the last hovered child. |
| */ |
| private int mLastHoveredChildVirtualViewId; |
| |
| /** |
| * Whether the increment virtual button is pressed. |
| */ |
| private boolean mIncrementVirtualButtonPressed; |
| |
| /** |
| * Whether the decrement virtual button is pressed. |
| */ |
| private boolean mDecrementVirtualButtonPressed; |
| |
| /** |
| * Provider to report to clients the semantic structure of this widget. |
| */ |
| private AccessibilityNodeProviderImpl mAccessibilityNodeProvider; |
| |
| /** |
| * Helper class for managing pressed state of the virtual buttons. |
| */ |
| private final PressedStateHelper mPressedStateHelper; |
| |
| /** |
| * 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. |
| */ |
| private boolean mHideWheelUntilFocused; |
| |
| /** |
| * Interface to listen for changes of the current value. |
| */ |
| public interface OnValueChangeListener { |
| |
| /** |
| * 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. |
| */ |
| public interface OnScrollListener { |
| /** @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. |
| */ |
| public static int SCROLL_STATE_IDLE = 0; |
| |
| /** |
| * The user is scrolling using touch, and their 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. |
| */ |
| public static int SCROLL_STATE_FLING = 2; |
| |
| /** |
| * 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. |
| */ |
| public interface Formatter { |
| |
| /** |
| * 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. |
| * |
| * @param context The application environment. |
| */ |
| public NumberPicker(Context context) { |
| this(context, null); |
| } |
| |
| /** |
| * Create a new number picker. |
| * |
| * @param context The application environment. |
| * @param attrs A collection of attributes. |
| */ |
| public NumberPicker(Context context, AttributeSet attrs) { |
| this(context, attrs, R.attr.numberPickerStyle); |
| } |
| |
| /** |
| * 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 |
| * |
| * @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); |
| saveAttributeDataForStyleable(context, R.styleable.NumberPicker, |
| attrs, attributesArray, 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.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. |
| * |
| * @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); |
| onScrollerFinished(mFlingScroller); |
| onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); |
| } else if (!mAdjustScroller.isFinished()) { |
| mFlingScroller.forceFinished(true); |
| mAdjustScroller.forceFinished(true); |
| onScrollerFinished(mAdjustScroller); |
| } 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: |
| case KeyEvent.KEYCODE_NUMPAD_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. |
| * |
| * @param onValueChangedListener The listener. |
| */ |
| public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) { |
| mOnValueChangeListener = onValueChangedListener; |
| } |
| |
| /** |
| * 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. |
| * <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. |
| * <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 more 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 more 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. |
| */ |
| private void showSoftInput() { |
| InputMethodManager inputMethodManager = |
| getContext().getSystemService(InputMethodManager.class); |
| 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. |
| */ |
| private void hideSoftInput() { |
| InputMethodManager inputMethodManager = |
| getContext().getSystemService(InputMethodManager.class); |
| if (inputMethodManager != null) { |
| inputMethodManager.hideSoftInputFromView(mInputText, 0); |
| } |
| if (mHasSelectorWheel) { |
| mInputText.setVisibility(View.INVISIBLE); |
| } |
| } |
| |
| /** |
| * 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. |
| * |
| * @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 {@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 {@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. |
| * <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. |
| * |
| * @return The value. |
| */ |
| public int getValue() { |
| return mValue; |
| } |
| |
| /** |
| * Returns the min value of the picker. |
| * |
| * @return The min value |
| */ |
| public int getMinValue() { |
| return mMinValue; |
| } |
| |
| /** |
| * Sets the min value of the picker. |
| * |
| * @param minValue The min value inclusive. |
| * |
| * <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. |
| * |
| * @return The max value. |
| */ |
| public int getMaxValue() { |
| return mMaxValue; |
| } |
| |
| /** |
| * 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. |
| * |
| * @return The displayed values. |
| */ |
| public String[] getDisplayedValues() { |
| return mDisplayedValues; |
| } |
| |
| /** |
| * 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 |
| */ |
| @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()); |
| } |
| |
| /** |
| * Set the height for the divider that separates the currently selected value from the others. |
| * @param height The height to be set |
| */ |
| public void setSelectionDividerHeight(@IntRange(from = 0) @Px int height) { |
| mSelectionDividerHeight = height; |
| invalidate(); |
| } |
| |
| /** |
| * Retrieve the height for the divider that separates the currently selected value from the |
| * others. |
| * @return The height of the divider |
| */ |
| @Px |
| public int getSelectionDividerHeight() { |
| return mSelectionDividerHeight; |
| } |
| |
| @Override |
| protected float getTopFadingEdgeStrength() { |
| return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; |
| } |
| |
| @Override |
| protected float getBottomFadingEdgeStrength() { |
| return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH; |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| removeAllCallbacks(); |
| } |
| |
| @CallSuper |
| @Override |
| protected void drawableStateChanged() { |
| super.drawableStateChanged(); |
| |
| final Drawable selectionDivider = mSelectionDivider; |
| if (selectionDivider != null && selectionDivider.isStateful() |
| && selectionDivider.setState(getDrawableState())) { |
| invalidateDrawable(selectionDivider); |
| } |
| } |
| |
| @CallSuper |
| @Override |
| public void jumpDrawablesToCurrentState() { |
| super.jumpDrawablesToCurrentState(); |
| |
| if (mSelectionDivider != null) { |
| mSelectionDivider.jumpToCurrentState(); |
| } |
| } |
| |
| /** @hide */ |
| @Override |
| public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) { |
| super.onResolveDrawables(layoutDirection); |
| |
| if (mSelectionDivider != null) { |
| mSelectionDivider.setLayoutDirection(layoutDirection); |
| } |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| if (!mHasSelectorWheel) { |
| super.onDraw(canvas); |
| return; |
| } |
| final boolean showSelectorWheel = mHideWheelUntilFocused ? hasFocus() : true; |
| float x = (mRight - mLeft) / 2; |
| float y = mCurrentScrollOffset; |
| |
| // draw the virtual buttons pressed state if needed |
| if (showSelectorWheel && mVirtualButtonPressedDrawable != null |
| && mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { |
| if (mDecrementVirtualButtonPressed) { |
| mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET); |
| mVirtualButtonPressedDrawable.setBounds(0, 0, mRight, mTopSelectionDividerTop); |
| mVirtualButtonPressedDrawable.draw(canvas); |
| } |
| if (mIncrementVirtualButtonPressed) { |
| mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET); |
| mVirtualButtonPressedDrawable.setBounds(0, mBottomSelectionDividerBottom, mRight, |
| mBottom); |
| mVirtualButtonPressedDrawable.draw(canvas); |
| } |
| } |
| |
| // draw the selector wheel |
| int[] selectorIndices = mSelectorIndices; |
| for (int i = 0; i < selectorIndices.length; i++) { |
| int selectorIndex = selectorIndices[i]; |
| String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); |
| // Do not draw the middle item if input is visible since the input |
| // is shown only if the wheel is static and it covers the middle |
| // item. Otherwise, if the user starts editing the text via the |
| // IME they may see a dimmed version of the old value intermixed |
| // with the new one. |
| if ((showSelectorWheel && i != SELECTOR_MIDDLE_ITEM_INDEX) || |
| (i == SELECTOR_MIDDLE_ITEM_INDEX && mInputText.getVisibility() != VISIBLE)) { |
| canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint); |
| } |
| y += mSelectorElementHeight; |
| } |
| |
| // draw the selection dividers |
| if (showSelectorWheel && mSelectionDivider != null) { |
| // draw the top divider |
| int topOfTopDivider = mTopSelectionDividerTop; |
| int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight; |
| mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider); |
| mSelectionDivider.draw(canvas); |
| |
| // draw the bottom divider |
| int bottomOfBottomDivider = mBottomSelectionDividerBottom; |
| int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight; |
| mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider); |
| mSelectionDivider.draw(canvas); |
| } |
| } |
| |
| /** @hide */ |
| @Override |
| public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) { |
| super.onInitializeAccessibilityEventInternal(event); |
| event.setClassName(NumberPicker.class.getName()); |
| event.setScrollable(true); |
| event.setScrollY((mMinValue + mValue) * mSelectorElementHeight); |
| event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight); |
| } |
| |
| @Override |
| public AccessibilityNodeProvider getAccessibilityNodeProvider() { |
| if (!mHasSelectorWheel) { |
| return super.getAccessibilityNodeProvider(); |
| } |
| if (mAccessibilityNodeProvider == null) { |
| mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl(); |
| } |
| return mAccessibilityNodeProvider; |
| } |
| |
| /** |
| * Sets the text color for all the states (normal, selected, focused) to be the given color. |
| * |
| * @param color A color value in the form 0xAARRGGBB. |
| */ |
| public void setTextColor(@ColorInt int color) { |
| mSelectorWheelPaint.setColor(color); |
| mInputText.setTextColor(color); |
| invalidate(); |
| } |
| |
| /** |
| * @return the text color. |
| */ |
| @ColorInt |
| public int getTextColor() { |
| return mSelectorWheelPaint.getColor(); |
| } |
| |
| /** |
| * Sets the text size to the given value. This value must be > 0 |
| * |
| * @param size The size in pixel units. |
| */ |
| public void setTextSize(@FloatRange(from = 0.0, fromInclusive = false) float size) { |
| mSelectorWheelPaint.setTextSize(size); |
| mInputText.setTextSize(TypedValue.COMPLEX_UNIT_PX, size); |
| invalidate(); |
| } |
| |
| /** |
| * @return the size (in pixels) of the text size in this NumberPicker. |
| */ |
| @FloatRange(from = 0.0, fromInclusive = false) |
| public float getTextSize() { |
| return mSelectorWheelPaint.getTextSize(); |
| } |
| |
| /** |
| * Makes a measure spec that tries greedily to use the max value. |
| * |
| * @param measureSpec The measure spec. |
| * @param maxSize The max value for the size. |
| * @return A measure spec greedily imposing the max size. |
| */ |
| private int makeMeasureSpec(int measureSpec, int maxSize) { |
| if (maxSize == SIZE_UNSPECIFIED) { |
| return measureSpec; |
| } |
| final int size = MeasureSpec.getSize(measureSpec); |
| final int mode = MeasureSpec.getMode(measureSpec); |
| switch (mode) { |
| case MeasureSpec.EXACTLY: |
| return measureSpec; |
| case MeasureSpec.AT_MOST: |
| return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY); |
| case MeasureSpec.UNSPECIFIED: |
| return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY); |
| default: |
| throw new IllegalArgumentException("Unknown measure mode: " + mode); |
| } |
| } |
| |
| /** |
| * Utility to reconcile a desired size and state, with constraints imposed |
| * by a MeasureSpec. Tries to respect the min size, unless a different size |
| * is imposed by the constraints. |
| * |
| * @param minSize The minimal desired size. |
| * @param measuredSize The currently measured size. |
| * @param measureSpec The current measure spec. |
| * @return The resolved size and state. |
| */ |
| private int resolveSizeAndStateRespectingMinSize( |
| int minSize, int measuredSize, int measureSpec) { |
| if (minSize != SIZE_UNSPECIFIED) { |
| final int desiredWidth = Math.max(minSize, measuredSize); |
| return resolveSizeAndState(desiredWidth, measureSpec, 0); |
| } else { |
| return measuredSize; |
| } |
| } |
| |
| /** |
| * Resets the selector indices and clear the cached string representation of |
| * these indices. |
| */ |
| @UnsupportedAppUsage |
| private void initializeSelectorWheelIndices() { |
| mSelectorIndexToStringCache.clear(); |
| int[] selectorIndices = mSelectorIndices; |
| int current = getValue(); |
| for (int i = 0; i < mSelectorIndices.length; i++) { |
| int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX); |
| if (mWrapSelectorWheel) { |
| selectorIndex = getWrappedSelectorIndex(selectorIndex); |
| } |
| selectorIndices[i] = selectorIndex; |
| ensureCachedScrollSelectorValue(selectorIndices[i]); |
| } |
| } |
| |
| /** |
| * Sets the current value of this NumberPicker. |
| * |
| * @param current The new value of the NumberPicker. |
| * @param notifyChange Whether to notify if the current value changed. |
| */ |
| private void setValueInternal(int current, boolean notifyChange) { |
| if (mValue == current) { |
| return; |
| } |
| // Wrap around the values if we go past the start or end |
| if (mWrapSelectorWheel) { |
| current = getWrappedSelectorIndex(current); |
| } else { |
| current = Math.max(current, mMinValue); |
| current = Math.min(current, mMaxValue); |
| } |
| int previous = mValue; |
| mValue = current; |
| // If we're flinging, we'll update the text view at the end when it becomes visible |
| if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) { |
| updateInputTextView(); |
| } |
| if (notifyChange) { |
| notifyChange(previous, current); |
| } |
| initializeSelectorWheelIndices(); |
| invalidate(); |
| } |
| |
| /** |
| * Changes the current value by one which is increment or |
| * decrement based on the passes argument. |
| * decrement the current value. |
| * |
| * @param increment True to increment, false to decrement. |
| */ |
| @UnsupportedAppUsage |
| 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 <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 <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 <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(); |
| } |
| |
| /** |
| * @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 <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 <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 <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 {@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. |
| */ |
| 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. |
| * |
| * @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. |
| */ |
| private void removeChangeCurrentByOneFromLongPress() { |
| if (mChangeCurrentByOneFromLongPressCommand != null) { |
| removeCallbacks(mChangeCurrentByOneFromLongPressCommand); |
| } |
| } |
| |
| /** |
| * 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. |
| */ |
| private void removeBeginSoftInputCommand() { |
| if (mBeginSoftInputOnLongPressCommand != null) { |
| removeCallbacks(mBeginSoftInputOnLongPressCommand); |
| } |
| } |
| |
| /** |
| * 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(); |
| } |
| |
| /** |
| * @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 {@link SetSelectionCommand} from the given |
| * {@code selectionStart} to {@code selectionEnd}. |
| */ |
| private void postSetSelectionCommand(int selectionStart, int selectionEnd) { |
| if (mSetSelectionCommand == null) { |
| mSetSelectionCommand = new SetSelectionCommand(mInputText); |
| } |
| mSetSelectionCommand.post(selectionStart, selectionEnd); |
| } |
| |
| /** |
| * The numbers accepted by the input text's {@link Filter} |
| */ |
| private static final char[] DIGIT_CHARACTERS = new char[] { |
| // Latin digits are the common case |
| '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', |
| // Arabic-Indic |
| '\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668' |
| , '\u0669', |
| // Extended Arabic-Indic |
| '\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5', '\u06f6', '\u06f7', '\u06f8' |
| , '\u06f9', |
| // Hindi and Marathi (Devanagari script) |
| '\u0966', '\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e' |
| , '\u096f', |
| // Bengali |
| '\u09e6', '\u09e7', '\u09e8', '\u09e9', '\u09ea', '\u09eb', '\u09ec', '\u09ed', '\u09ee' |
| , '\u09ef', |
| // Kannada |
| '\u0ce6', '\u0ce7', '\u0ce8', '\u0ce9', '\u0cea', '\u0ceb', '\u0cec', '\u0ced', '\u0cee' |
| , '\u0cef' |
| }; |
| |
| /** |
| * Filter for accepting only valid indices or prefixes of the string |
| * representation of valid indices. |
| */ |
| class InputTextFilter extends NumberKeyListener { |
| |
| // XXX This doesn't allow for range limits when controlled by a |
| // soft input method! |
| public int getInputType() { |
| return InputType.TYPE_CLASS_TEXT; |
| } |
| |
| @Override |
| protected char[] getAcceptedChars() { |
| return DIGIT_CHARACTERS; |
| } |
| |
| @Override |
| public CharSequence filter( |
| CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { |
| // We don't know what the output will be, so always cancel any |
| // pending set selection command. |
| if (mSetSelectionCommand != null) { |
| mSetSelectionCommand.cancel(); |
| } |
| |
| if (mDisplayedValues == null) { |
| CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); |
| if (filtered == null) { |
| filtered = source.subSequence(start, end); |
| } |
| |
| String result = String.valueOf(dest.subSequence(0, dstart)) + filtered |
| + dest.subSequence(dend, dest.length()); |
| |
| if ("".equals(result)) { |
| return result; |
| } |
| int val = getSelectedPos(result); |
| |
| /* |
| * Ensure the user can't type in a value greater than the max |
| * allowed. We have to allow less than min as the user might |
| * want to delete some numbers and then type a new number. |
| * And prevent multiple-"0" that exceeds the length of upper |
| * bound number. |
| */ |
| if (val > mMaxValue || result.length() > String.valueOf(mMaxValue).length()) { |
| return ""; |
| } else { |
| return filtered; |
| } |
| } else { |
| CharSequence filtered = String.valueOf(source.subSequence(start, end)); |
| if (TextUtils.isEmpty(filtered)) { |
| return ""; |
| } |
| String result = String.valueOf(dest.subSequence(0, dstart)) + filtered |
| + dest.subSequence(dend, dest.length()); |
| String str = String.valueOf(result).toLowerCase(); |
| for (String val : mDisplayedValues) { |
| String valLowerCase = val.toLowerCase(); |
| if (valLowerCase.startsWith(str)) { |
| postSetSelectionCommand(result.length(), val.length()); |
| return val.subSequence(dstart, val.length()); |
| } |
| } |
| return ""; |
| } |
| } |
| } |
| |
| /** |
| * Ensures that the scroll wheel is adjusted i.e. there is no offset and the |
| * middle element is in the middle of the widget. |
| * |
| * @return Whether an adjustment has been made. |
| */ |
| private boolean ensureScrollWheelAdjusted() { |
| // adjust to the closest value |
| int deltaY = mInitialScrollOffset - mCurrentScrollOffset; |
| if (deltaY != 0) { |
| mPreviousScrollerY = 0; |
| if (Math.abs(deltaY) > mSelectorElementHeight / 2) { |
| deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight; |
| } |
| mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS); |
| invalidate(); |
| return true; |
| } |
| return false; |
| } |
| |
| class PressedStateHelper implements Runnable { |
| public static final int BUTTON_INCREMENT = 1; |
| public static final int BUTTON_DECREMENT = 2; |
| |
| private final int MODE_PRESS = 1; |
| private final int MODE_TAPPED = 2; |
| |
| private int mManagedButton; |
| private int mMode; |
| |
| public void cancel() { |
| mMode = 0; |
| mManagedButton = 0; |
| NumberPicker.this.removeCallbacks(this); |
| if (mIncrementVirtualButtonPressed) { |
| mIncrementVirtualButtonPressed = false; |
| invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); |
| } |
| mDecrementVirtualButtonPressed = false; |
| if (mDecrementVirtualButtonPressed) { |
| invalidate(0, 0, mRight, mTopSelectionDividerTop); |
| } |
| } |
| |
| public void buttonPressDelayed(int button) { |
| cancel(); |
| mMode = MODE_PRESS; |
| mManagedButton = button; |
| NumberPicker.this.postDelayed(this, ViewConfiguration.getTapTimeout()); |
| } |
| |
| public void buttonTapped(int button) { |
| cancel(); |
| mMode = MODE_TAPPED; |
| mManagedButton = button; |
| NumberPicker.this.post(this); |
| } |
| |
| @Override |
| public void run() { |
| switch (mMode) { |
| case MODE_PRESS: { |
| switch (mManagedButton) { |
| case BUTTON_INCREMENT: { |
| mIncrementVirtualButtonPressed = true; |
| invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); |
| } break; |
| case BUTTON_DECREMENT: { |
| mDecrementVirtualButtonPressed = true; |
| invalidate(0, 0, mRight, mTopSelectionDividerTop); |
| } |
| } |
| } break; |
| case MODE_TAPPED: { |
| switch (mManagedButton) { |
| case BUTTON_INCREMENT: { |
| if (!mIncrementVirtualButtonPressed) { |
| NumberPicker.this.postDelayed(this, |
| ViewConfiguration.getPressedStateDuration()); |
| } |
| mIncrementVirtualButtonPressed ^= true; |
| invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom); |
| } break; |
| case BUTTON_DECREMENT: { |
| if (!mDecrementVirtualButtonPressed) { |
| NumberPicker.this.postDelayed(this, |
| ViewConfiguration.getPressedStateDuration()); |
| } |
| mDecrementVirtualButtonPressed ^= true; |
| invalidate(0, 0, mRight, mTopSelectionDividerTop); |
| } |
| } |
| } break; |
| } |
| } |
| } |
| |
| /** |
| * Command for setting the input text selection. |
| */ |
| private static class SetSelectionCommand implements Runnable { |
| private final EditText mInputText; |
| |
| private int mSelectionStart; |
| private int mSelectionEnd; |
| |
| /** Whether this runnable is currently posted. */ |
| private boolean mPosted; |
| |
| public SetSelectionCommand(EditText inputText) { |
| mInputText = inputText; |
| } |
| |
| public void post(int selectionStart, int selectionEnd) { |
| mSelectionStart = selectionStart; |
| mSelectionEnd = selectionEnd; |
| |
| if (!mPosted) { |
| mInputText.post(this); |
| mPosted = true; |
| } |
| } |
| |
| public void cancel() { |
| if (mPosted) { |
| mInputText.removeCallbacks(this); |
| mPosted = false; |
| } |
| } |
| |
| @Override |
| public void run() { |
| mPosted = false; |
| mInputText.setSelection(mSelectionStart, mSelectionEnd); |
| } |
| } |
| |
| /** |
| * Command for changing the current value from a long press by one. |
| */ |
| class ChangeCurrentByOneFromLongPressCommand implements Runnable { |
| private boolean mIncrement; |
| |
| private void setStep(boolean increment) { |
| mIncrement = increment; |
| } |
| |
| @Override |
| public void run() { |
| changeValueByOne(mIncrement); |
| postDelayed(this, mLongPressUpdateInterval); |
| } |
| } |
| |
| /** |
| * @hide |
| */ |
| public static class CustomEditText extends EditText { |
| |
| public CustomEditText(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| } |
| |
| @Override |
| public void onEditorAction(int actionCode) { |
| super.onEditorAction(actionCode); |
| if (actionCode == EditorInfo.IME_ACTION_DONE) { |
| clearFocus(); |
| } |
| } |
| } |
| |
| /** |
| * Command for beginning soft input on long press. |
| */ |
| class BeginSoftInputOnLongPressCommand implements Runnable { |
| |
| @Override |
| public void run() { |
| performLongClick(); |
| } |
| } |
| |
| /** |
| * Class for managing virtual view tree rooted at this picker. |
| */ |
| class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider { |
| private static final int UNDEFINED = Integer.MIN_VALUE; |
| |
| private static final int VIRTUAL_VIEW_ID_INCREMENT = 1; |
| |
| private static final int VIRTUAL_VIEW_ID_INPUT = 2; |
| |
| private static final int VIRTUAL_VIEW_ID_DECREMENT = 3; |
| |
| private final Rect mTempRect = new Rect(); |
| |
| private final int[] mTempArray = new int[2]; |
| |
| private int mAccessibilityFocusedView = UNDEFINED; |
| |
| @Override |
| public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { |
| switch (virtualViewId) { |
| case View.NO_ID: |
| return createAccessibilityNodeInfoForNumberPicker( mScrollX, mScrollY, |
| mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop)); |
| case VIRTUAL_VIEW_ID_DECREMENT: |
| return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT, |
| getVirtualDecrementButtonText(), mScrollX, mScrollY, |
| mScrollX + (mRight - mLeft), |
| mTopSelectionDividerTop + mSelectionDividerHeight); |
| case VIRTUAL_VIEW_ID_INPUT: |
| return createAccessibiltyNodeInfoForInputText(mScrollX, |
| mTopSelectionDividerTop + mSelectionDividerHeight, |
| mScrollX + (mRight - mLeft), |
| mBottomSelectionDividerBottom - mSelectionDividerHeight); |
| case VIRTUAL_VIEW_ID_INCREMENT: |
| return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT, |
| getVirtualIncrementButtonText(), mScrollX, |
| mBottomSelectionDividerBottom - mSelectionDividerHeight, |
| mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop)); |
| } |
| return super.createAccessibilityNodeInfo(virtualViewId); |
| } |
| |
| @Override |
| public List<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: |
| case R.id.accessibilityActionScrollDown: { |
| if (NumberPicker.this.isEnabled() |
| && (getWrapSelectorWheel() || getValue() < getMaxValue())) { |
| changeValueByOne(true); |
| return true; |
| } |
| } return false; |
| case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: |
| case R.id.accessibilityActionScrollUp: { |
| 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); |
| info.setAccessibilityFocused(mAccessibilityFocusedView == 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()); |
| info.setAccessibilityFocused(mAccessibilityFocusedView == virtualViewId); |
| 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); |
| info.setAccessibilityFocused(mAccessibilityFocusedView == View.NO_ID); |
| |
| 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.AccessibilityAction.ACTION_SCROLL_FORWARD); |
| info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN); |
| } |
| if (getWrapSelectorWheel() || getValue() > getMinValue()) { |
| info.addAction( |
| AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); |
| info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP); |
| } |
| } |
| |
| 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); |
| } |
| } |