2925 lines
110 KiB
Java
2925 lines
110 KiB
Java
![]() |
/*
|
||
|
* 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);
|
||
|
}
|
||
|
}
|