Skip to content

Instantly share code, notes, and snippets.

@Miha-x64
Last active December 3, 2021 13:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Miha-x64/c9f473b7095f0c1dac6c36c18bde5b77 to your computer and use it in GitHub Desktop.
Save Miha-x64/c9f473b7095f0c1dac6c36c18bde5b77 to your computer and use it in GitHub Desktop.
NumberPicker fork supporting gravity, scale and alpha interpolation, typeface styling, and more.
/*
* 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.
*/
import android.animation.TimeInterpolator;
import android.annotation.SuppressLint;
import android.content.Context;
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.Point;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
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.Gravity;
import android.view.KeyEvent;
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 android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.NumberPicker.Formatter;
import android.widget.Scroller;
import android.widget.TextView;
import androidx.annotation.CallSuper;
import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.Keep;
import androidx.annotation.Px;
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;
import static android.widget.NumberPicker.OnScrollListener.SCROLL_STATE_FLING;
import static android.widget.NumberPicker.OnScrollListener.SCROLL_STATE_IDLE;
import static android.widget.NumberPicker.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL;
/**
* A widget that enables the user to select a number from a predefined range.
*
* 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.
*
* <a href="https://gist.github.com/c9f473b7095f0c1dac6c36c18bde5b77">This Gist permalink.</a>
*/
public class FlexibleNumberPicker extends FrameLayout {
/**
* User choice on whether the selector wheel should be wrapped.
*/
private boolean mWrapSelectorWheelPreferred = true;
/**
* The text for showing the current value.
*/
// final EditText mInputText;
/**
* The min height of this widget.
*/
private final int mMinHeight;
/**
* The max height of this widget.
*/
private final int mMaxHeight;
/**
* The max width of this widget.
*/
private final int mMinWidth;
/**
* The max width of this widget.
*/
private int mMaxWidth;
/**
* Flag whether to compute the max width.
*/
private final boolean mComputeMaxWidth;
/**
* The height of the gap between text elements if the selector wheel.
*/
private int mSelectorTextGapHeight;
/**
* The values to be displayed instead the indices.
*/
String[] mDisplayedValues;
/**
* Lower value of the range of numbers allowed for the NumberPicker
*/
int mMinValue;
/**
* Upper value of the range of numbers allowed for the NumberPicker
*/
int mMaxValue;
/**
* Current value of this NumberPicker
*/
int mValue;
/**
* Listener to be notified upon current value change.
*/
private OnValueChangeListener mOnValueChangeListener;
/**
* Listener to be notified upon scroll state change.
*/
private OnScrollListener mOnScrollListener;
/**
* Formatter for displaying the current value.
*/
private Formatter mFormatter;
/**
* The speed for updating the value form long press.
*/
long mLongPressUpdateInterval = 300;
/**
* Cache for the string representation of selector indices.
*/
private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<>();
/**
* The selector indices whose value are show by the selector.
*/
private int[] mSelectorIndices = new int[3];
/**
* The {@link Paint} for drawing the selector.
*/
private final Paint mSelectorWheelPaint;
/**
* 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.
*/
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.
*/
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 final int mTouchSlop;
/**
* @see ViewConfiguration#getScaledMinimumFlingVelocity()
*/
private final int mMinimumFlingVelocity;
/**
* @see ViewConfiguration#getScaledMaximumFlingVelocity()
*/
private final int mMaximumFlingVelocity;
/**
* Flag whether the selector should wrap around.
*/
boolean mWrapSelectorWheel;
/**
* The back ground color used to optimize scroller fading.
*/
private int mSolidColor = Color.TRANSPARENT;
/**
* Divider for showing item to be selected while scrolling
*/
Drawable mSelectionDivider;
/**
* The current scroll state of the number picker.
*/
private int mScrollState = 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.
*/
int mTopSelectionDividerTop;
/**
* The bottom of the bottom selection divider.
*/
int mBottomSelectionDividerBottom;
/**
* The virtual id of the last hovered child.
*/
private int mLastHoveredChildVirtualViewId;
/**
* Whether the increment virtual button is pressed.
*/
boolean mIncrementVirtualButtonPressed;
/**
* Whether the decrement virtual button is pressed.
*/
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;
private float dividerOffset = .5f;
private static final TimeInterpolator RETARDED_INTERPOLATOR = (value) -> 1f;
private TimeInterpolator scaleInterpolator = RETARDED_INTERPOLATOR;
private TimeInterpolator alphaInterpolator = RETARDED_INTERPOLATOR;
private float lineHeight;
/**
* 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(FlexibleNumberPicker picker, int oldVal, int newVal);
}
/**
* Interface to listen for the picker scroll state.
*/
public interface OnScrollListener {
@IntDef({SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING})
@Retention(RetentionPolicy.SOURCE)
@interface ScrollState {}
/**
* 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.
*/
void onScrollStateChange(FlexibleNumberPicker view, @ScrollState int scrollState);
}
/**
* Create a new number picker.
*
* @param context The application environment.
*/
public FlexibleNumberPicker(Context context) {
this(context, null);
}
/**
* Create a new number picker.
*
* @param context The application environment.
* @param attrs A collection of attributes.
*/
public FlexibleNumberPicker(Context context, AttributeSet attrs) {
super(context, attrs);
float dp = getResources().getDisplayMetrics().density;
final Drawable selectionDivider = new ColorDrawable(Color.BLACK);
selectionDivider.setBounds(0, 0, 0, (int) (2 * dp));
setSelectionDivider(selectionDivider);
mMinHeight = -1;
mMaxHeight = (int) (180 * dp);
mMinWidth = (int) (64 * dp);
mMaxWidth = -1;
mComputeMaxWidth = mMaxWidth == -1;
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(false);
/*
class="android.widget.NumberPicker$CustomEditText"
android:id="@+id/numberpicker_input"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:singleLine="true"
android:background="@null"
android:textAppearance="@style/TextAppearance.Material.NumberPicker" />
*/
/*mInputText = new CustomEditText(context);
mInputText.setGravity(Gravity.CENTER);
mInputText.setSingleLine(true);
mInputText.setBackground(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mInputText.setTextAppearance(android.R.style.TextAppearance_Material_Body1);
} else {
//noinspection deprecation
mInputText.setTextAppearance(context, android.R.style.TextAppearance_Material_Body1);
}
addView(mInputText, new LayoutParams(MATCH_PARENT, WRAP_CONTENT, Gravity.CENTER_VERTICAL));
// input text
mInputText.setOnFocusChangeListener((v, 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() / 8;
// create the selector wheel paint
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setTextAlign(Align.CENTER);
TypedArray ta = context.obtainStyledAttributes(
null, new int[] { android.R.attr.textSize, android.R.attr.textColor },
android.R.attr.textAppearance, android.R.style.TextAppearance_Material_Widget_TextView);
paint.setTextSize(lineHeight = ta.getDimensionPixelSize(0, 0));
paint.setColor(ta.getColorStateList(1).getColorForState(ENABLED_STATE_SET, Color.BLACK));
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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && getFocusable() == View.FOCUSABLE_AUTO) {
setFocusable(View.FOCUSABLE);
setFocusableInTouchMode(true);
}
}
/**
* Set divider above and below current selection. Use intrinsic height, if possible.
*/
public void setSelectionDividerWithIntrinsicHeight(Drawable selectionDivider) { // ADDED
if (selectionDivider != null && selectionDivider.getIntrinsicHeight() > 0) {
selectionDivider.setBounds(0, 0, 0, selectionDivider.getIntrinsicHeight());
}
setSelectionDivider(selectionDivider);
}
/**
* Set divider above and below current selection.
* Reuses height from selectionDivider drawable's bounds, make sure to set it.
*/
public void setSelectionDivider(Drawable selectionDivider) { // ADDED
if (mSelectionDivider != selectionDivider) {
if (mSelectionDivider != null)
mSelectionDivider.setCallback(null);
mSelectionDivider = selectionDivider;
if (selectionDivider != null) {
selectionDivider.setCallback(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
selectionDivider.setLayoutDirection(getLayoutDirection());
if (selectionDivider.isStateful())
selectionDivider.setState(getDrawableState());
}
invalidate();
}
}
/**
* Update selector wheel paint.
*/
@SuppressLint("WrongConstant") // Typeface.BOLD_ITALIC is actually Typeface.BOLD | Typeface.ITALIC
public void setSelectorWheelPaint(Paint copyFrom) { // ADDED
boolean requestLayout = Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
!mSelectorWheelPaint.equalsForTextMeasurement(copyFrom);
setGravity(gravity(copyFrom));
mSelectorWheelPaint.set(copyFrom);
/*mInputText.setTextScaleX(copyFrom.getTextScaleX());
mInputText.setTypeface(
copyFrom.getTypeface(),
(copyFrom.isFakeBoldText() ? Typeface.BOLD : Typeface.NORMAL) |
(copyFrom.getTextSkewX() == -.25f ? Typeface.ITALIC : Typeface.NORMAL));
mInputText.setElegantTextHeight(copyFrom.isElegantTextHeight());
mInputText.setLetterSpacing(copyFrom.getLetterSpacing());
mInputText.setFontFeatureSettings(copyFrom.getFontFeatureSettings());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
mInputText.setFontVariationSettings(copyFrom.getFontVariationSettings());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
mInputText.setShadowLayer(
copyFrom.getShadowLayerRadius(), copyFrom.getShadowLayerDx(),
copyFrom.getShadowLayerDy(), copyFrom.getShadowLayerColor()
);
mInputText.setPaintFlags(copyFrom.getFlags());
mInputText.setTextSize(TypedValue.COMPLEX_UNIT_PX, copyFrom.getTextSize());
mInputText.setTextColor(copyFrom.getColor());*/
if (requestLayout) requestLayout();
invalidate();
}
/**
* Update selector wheel shadowLayer.
* This is useful for SDK<29 where paint.getShadowLayer*() methods were introduced.
* {@link #setSelectorWheelPaint} will copy shadowLayer properties properly only since SDK29.
*/
public void setShadowLayer(@Px float radius, @Px float dx, @Px float dy, @ColorInt int color) { // ADDED
mSelectorWheelPaint.setShadowLayer(radius, dx, dy, color);
// mInputText.setShadowLayer(radius, dx, dy, color);
invalidate();
}
public void setGravity(int gravity) { // ADDED
Paint.Align align = align(gravity);
if (mSelectorWheelPaint.getTextAlign() != align) {
mSelectorWheelPaint.setTextAlign(align);
// mInputText.setGravity(gravity & Gravity.HORIZONTAL_GRAVITY_MASK | Gravity.CENTER_VERTICAL);
invalidate();
}
}
@SuppressLint("RtlHardcoded")
private static int gravity(Paint paint) { // ADDED
switch (paint.getTextAlign()) {
case LEFT: return Gravity.LEFT | Gravity.CENTER_VERTICAL;
case CENTER: return Gravity.CENTER;
case RIGHT: return Gravity.RIGHT | Gravity.CENTER_VERTICAL;
}
throw new AssertionError();
}
@SuppressLint("RtlHardcoded")
private static Paint.Align align(int gravity) { // ADDED
switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.LEFT: return Align.LEFT;
case Gravity.CENTER_HORIZONTAL: return Align.CENTER;
case Gravity.RIGHT: return Align.RIGHT;
default: throw new UnsupportedOperationException(); // too lazy for this shit
}
}
public void setWheelItemCount(int count) { // ADDED
if (mSelectorIndices.length != count) {
if (count < 1 || (count&1) == 0) throw new IllegalArgumentException("count must be positive odd, got " + count);
mSelectorIndices = new int[count];
initializeSelectorWheelIndices();
requestLayout();
}
}
public void setDividerOffset(@FloatRange(from = 0f, to = 1f) float dividerOffset) { // ADDED
if (this.dividerOffset != dividerOffset) {
if (dividerOffset < 0f || dividerOffset > 1f)
throw new IllegalArgumentException("dividerOffset " + dividerOffset + " overlaps text");
this.dividerOffset = dividerOffset;
invalidate();
}
}
public void setScaleInterpolator(TimeInterpolator scaleInterpolator) { // ADDED
if (scaleInterpolator == null) scaleInterpolator = RETARDED_INTERPOLATOR;
if (this.scaleInterpolator != scaleInterpolator) {
this.scaleInterpolator = scaleInterpolator;
invalidate();
}
}
public void setAlphaInterpolator(TimeInterpolator alphaInterpolator) { // ADDED
if (alphaInterpolator == null) alphaInterpolator = RETARDED_INTERPOLATOR;
if (this.alphaInterpolator != alphaInterpolator) {
this.alphaInterpolator = alphaInterpolator;
invalidate();
}
}
public void setLineHeight(int unit, float value) { // ADDED
value = TypedValue.applyDimension(unit, value, getResources().getDisplayMetrics());
if (this.lineHeight != value) {
this.lineHeight = value;
initializeSelectorWheel();
invalidate();
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
/*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();
float offset = dividerOffset * mSelectorTextGapHeight + mSelectorWheelPaint.getTextSize() / 2f +
(mSelectionDivider == null ? 0 : mSelectionDivider.getBounds().height());
float center = getHeight() / 2f;
mTopSelectionDividerTop = (int) (center - offset);
mBottomSelectionDividerBottom = (int) (center + offset);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 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 Whether 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 (!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 == SCROLL_STATE_IDLE) {
mPressedStateHelper.buttonPressDelayed(PressedStateHelper.BUTTON_DECREMENT);
}
} else if (mLastDownEventY > mBottomSelectionDividerBottom) {
if (mScrollState == 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(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()) {
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 != SCROLL_STATE_TOUCH_SCROLL) {
int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
if (deltaDownY > mTouchSlop) {
removeAllCallbacks();
onScrollStateChange(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(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) - mSelectorIndices.length/2;
if (selectorIndexOffset > 0) {
changeValueByOne(true);
mPressedStateHelper.buttonTapped(PressedStateHelper.BUTTON_INCREMENT);
} else if (selectorIndexOffset < 0) {
changeValueByOne(false);
mPressedStateHelper.buttonTapped(PressedStateHelper.BUTTON_DECREMENT);
}
}
} else {
ensureScrollWheelAdjusted();
}
onScrollStateChange(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:
switch (event.getAction()) {
case KeyEvent.ACTION_DOWN:
if (mWrapSelectorWheel ||
((keyCode == KeyEvent.KEYCODE_DPAD_DOWN) ? mValue < mMaxValue : mValue > mMinValue)) {
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 (accessibilityManager().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);
mInputText.setEnabled(enabled);
}*/
@Override
public void scrollBy(int x, int y) {
int[] selectorIndices = mSelectorIndices;
int startScrollOffset = mCurrentScrollOffset;
int midIndex = mSelectorIndices.length/2;
if (!mWrapSelectorWheel) {
int midSelectorIdx = selectorIndices[midIndex];
if ((y > 0 && midSelectorIdx <= mMinValue) || (y < 0 && midSelectorIdx >= mMaxValue)) {
mCurrentScrollOffset = mInitialScrollOffset;
return;
}
}
mCurrentScrollOffset += y;
while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorElementHeight/2) {
mCurrentScrollOffset -= mSelectorElementHeight;
decrementSelectorIndices(selectorIndices);
setValueInternal(selectorIndices[midIndex], true);
if (!mWrapSelectorWheel && selectorIndices[midIndex] <= mMinValue) {
mCurrentScrollOffset = mInitialScrollOffset;
}
}
while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorElementHeight/2) {
mCurrentScrollOffset += mSelectorElementHeight;
incrementSelectorIndices(selectorIndices);
setValueInternal(selectorIndices[midIndex], true);
if (!mWrapSelectorWheel && selectorIndices[midIndex] >= 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;
}
public void setSolidColor(@ColorInt int solidColor) {
mSolidColor = solidColor;
}
/**
* 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 #getMinValue()} and
* {@link #getWrapSelectorWheel()} is <code>false</code> the
* current value is set to the {@link #getMinValue()} value.
* </p>
* <p>
* If the argument is less than the {@link #getMinValue()} and
* {@link #getWrapSelectorWheel()} is <code>true</code> the
* current value is set to the {@link #getMaxValue()} value.
* </p>
* <p>
* If the argument is more than the {@link #getMaxValue()} and
* {@link #getWrapSelectorWheel()} is <code>false</code> the
* current value is set to the {@link #getMaxValue()} value.
* </p>
* <p>
* If the argument is more than the {@link #getMaxValue()} and
* {@link #getWrapSelectorWheel()} is <code>true</code> the
* current value is set to the {@link #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 (!super.performClick()) {
showSoftInput();
}
return true;
}
@Override
public boolean performLongClick() {
if (!super.performLongClick()) {
showSoftInput();
mIgnoreMoveEvents = true;
}
return true;
}
/**
* Shows the soft input for its input text.
*/
private void showSoftInput() {
/*InputMethodManager inputMethodManager = imm();
if (inputMethodManager != null) {
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 = imm();
if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) {
inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
}
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 {
for (String displayedValue : mDisplayedValues) {
final float textWidth = mSelectorWheelPaint.measureText(displayedValue);
if (textWidth > maxTextWidth) {
maxTextWidth = (int) textWidth;
}
}
}
// maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight();
if (mMaxWidth != maxTextWidth) {
mMaxWidth = Math.max(maxTextWidth, 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 #getMinValue()} and
* {@link #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;
}
onMinMaxChanged();
}
/**
* 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;
}
onMinMaxChanged();
}
private void onMinMaxChanged() {
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();
}
@Override
protected float getTopFadingEdgeStrength() {
return 1f;
}
@Override
protected float getBottomFadingEdgeStrength() {
return 1f;
}
@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();
}
}
/*@Override*/ @Keep
public void onResolveDrawables(int layoutDirection) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && mSelectionDivider != null) {
mSelectionDivider.setLayoutDirection(layoutDirection);
}
}
@Override
protected void onDraw(Canvas canvas) {
float x;
switch (mSelectorWheelPaint.getTextAlign()) {
case LEFT: x = getPaddingLeft(); break;
case CENTER: x = getWidth() / 2; break;
case RIGHT: x = getWidth() - getPaddingRight(); break;
default: throw new AssertionError();
}
float y = mCurrentScrollOffset;
// draw the selector wheel
int[] selectorIndices = mSelectorIndices;
int midIndex = selectorIndices.length / 2;
float lim = 2 * midIndex * mSelectorElementHeight; // distance between top and bottom text baselines
int originalAlpha = mSelectorWheelPaint.getAlpha();
for (int i = 0; i < selectorIndices.length; i++) {
canvas.save();
float midLine = y - (lineHeight - fmi.descent) / 2f;
float input = (midLine - mInitialScrollOffset - lim / 2) / lim;
float scale = scaleInterpolator.getInterpolation(input);
canvas.scale(1f, scale, x, midLine + (i < midIndex ? 1 - scale : -1 + scale) * mSelectorElementHeight);
mSelectorWheelPaint.setAlpha((int) (originalAlpha * alphaInterpolator.getInterpolation(input)));
// if (i != midIndex || mInputText.getVisibility() != VISIBLE)
canvas.drawText(mSelectorIndexToStringCache.get(selectorIndices[i]), x, y, mSelectorWheelPaint);
canvas.restore();
y += mSelectorElementHeight;
}
mSelectorWheelPaint.setAlpha(originalAlpha);
// draw the selection dividers
if (mSelectionDivider != null) {
int selectionDividerHeight = mSelectionDivider.getBounds().height();
// draw the top divider
int topOfTopDivider = mTopSelectionDividerTop;
int bottomOfTopDivider = topOfTopDivider + selectionDividerHeight;
mSelectionDivider.setBounds(0, topOfTopDivider, getRight(), bottomOfTopDivider);
mSelectionDivider.draw(canvas);
// draw the bottom divider
int bottomOfBottomDivider = mBottomSelectionDividerBottom;
int topOfBottomDivider = bottomOfBottomDivider - selectionDividerHeight;
mSelectionDivider.setBounds(0, topOfBottomDivider, getRight(), bottomOfBottomDivider);
mSelectionDivider.draw(canvas);
}
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || getAccessibilityDelegate() != null) {
event.setClassName(android.widget.NumberPicker.class.getName());
event.setScrollable(true);
event.setScrollY((mMinValue + mValue) * mSelectorElementHeight);
event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight);
}
}
@Override
public AccessibilityNodeProvider getAccessibilityNodeProvider() {
if (mAccessibilityNodeProvider == null) {
mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl();
}
return mAccessibilityNodeProvider;
}
/**
* Makes a measure spec that tries greedily to use the max value.
*
* @param measureSpec The measure spec.
* @param maxSize The max value for the size.
* @return A measure spec greedily imposing the max size.
*/
private static int makeMeasureSpec(int measureSpec, int maxSize) {
if (maxSize == -1) {
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 static int resolveSizeAndStateRespectingMinSize(int minSize, int measuredSize, int measureSpec) {
return minSize == -1 ? measuredSize : resolveSizeAndState(Math.max(minSize, measuredSize), measureSpec, 0);
}
/**
* Resets the selector indices and clear the cached string representation of
* these indices.
*/
private void initializeSelectorWheelIndices() {
mSelectorIndexToStringCache.clear();
int[] selectorIndices = mSelectorIndices;
int current = mValue;
int mid = selectorIndices.length/2;
for (int i = 0; i < mSelectorIndices.length; i++) {
int selectorIndex = current + (i - mid);
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 != SCROLL_STATE_FLING) {
updateInputTextView();
}
if (notifyChange) {
notifyChange(previous);
}
initializeSelectorWheelIndices();
invalidate();
}
/**
* Changes the current value by one which is increment or
* decrement based on the passed argument.
* decrement the current value.
*
* @param increment True to increment, false to decrement.
*/
public void changeValueByOne(boolean increment) {
hideSoftInput();
if (!moveToFinalScrollerPosition(mFlingScroller)) {
moveToFinalScrollerPosition(mAdjustScroller);
}
mPreviousScrollerY = 0;
mFlingScroller.startScroll(0, 0, 0, increment ? -mSelectorElementHeight : mSelectorElementHeight, 300);
invalidate();
}
private final Paint.FontMetricsInt fmi = new Paint.FontMetricsInt();
private void initializeSelectorWheel() {
initializeSelectorWheelIndices();
int selectorCount = mSelectorIndices.length;
mSelectorElementHeight = getHeight() / selectorCount;
float textSize = mSelectorWheelPaint.getTextSize();
float totalTextSize = selectorCount * textSize;
float totalGapSize = getHeight() - totalTextSize;
mSelectorTextGapHeight = (int) (totalGapSize / selectorCount);
// Ensure that the middle item is positioned the same as the text in mInputText
mSelectorWheelPaint.getFontMetricsInt(fmi);
int editTextTextPosition = (int) ((getHeight() + lineHeight) / 2f - fmi.descent); // mInputText.getBaseline() + mInputText.getTop();
mInitialScrollOffset = editTextTextPosition - (selectorCount /2 * mSelectorElementHeight);
mCurrentScrollOffset = mInitialScrollOffset;
updateInputTextView();
}
private void initializeFadingEdges() {
setVerticalFadingEdgeEnabled(true);
setFadingEdgeLength(getHeight() / 2);
}
/**
* Callback invoked upon completion of a given <code>scroller</code>.
*/
private void onScrollerFinished(Scroller scroller) {
if (scroller == mFlingScroller) {
ensureScrollWheelAdjusted();
updateInputTextView();
onScrollStateChange(SCROLL_STATE_IDLE);
} else if (mScrollState != 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.
*/
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) {
System.arraycopy(selectorIndices, 1, selectorIndices, 0, selectorIndices.length - 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) {
System.arraycopy(selectorIndices, 0, selectorIndices, 1, selectorIndices.length - 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);
}
String formatNumber(int value) {
return (mFormatter != null) ? mFormatter.format(value) : formatNumberWithLocale(value);
}
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);
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.
*/
private void 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().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(this, AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT);
requestSendAccessibilityEvent(this, event);
}
}
}*/
}
/**
* Notifies the listener, if registered, of a change of the value of this
* NumberPicker.
*/
private void notifyChange(int previous) {
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>.
*/
int getSelectedPos(String value) {
// Ignore as if it's not a number we don't care
if (mDisplayedValues != null) {
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}.
*/
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}
*/
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 = 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.
*/
private void 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, 800);
invalidate();
}
}
private InputMethodManager imm() {
return (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
}
AccessibilityManager accessibilityManager() {
return (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
}
private Rect tmpRect;
private Point tmpPoint;
protected boolean isVisibleToUser(Rect boundInView) {
// Attached to invisible window means this view is not visible.
if (getWindowVisibility() != View.VISIBLE) {
return false;
}
// An invisible predecessor or one with alpha zero means
// that this view is not visible to the user.
Object current = this;
while (current instanceof View) {
View view = (View) current;
// We have attach info so this view is attached and there is no
// need to check whether we reach to ViewRootImpl on the way up.
if (view.getAlpha() <= 0 ||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && view.getTransitionAlpha() <= 0) ||
view.getVisibility() != VISIBLE) {
return false;
}
current = view.getParent();
}
// Check if the view is entirely covered by its predecessors.
Rect visibleRect = tmpRect;
Point offset = tmpPoint;
if (visibleRect == null) {
tmpRect = visibleRect = new Rect();
tmpPoint = offset = new Point();
}
if (!getGlobalVisibleRect(visibleRect, offset)) {
return false;
}
// Check if the visible portion intersects the rectangle of interest.
if (boundInView != null) {
visibleRect.offset(-offset.x, -offset.y);
return boundInView.intersect(visibleRect);
}
return true;
}
class PressedStateHelper implements Runnable {
public static final int BUTTON_INCREMENT = 1;
public static final int BUTTON_DECREMENT = 2;
private static final int MODE_PRESS = 1;
private static final int MODE_TAPPED = 2;
private int mManagedButton;
private int mMode;
public void cancel() {
mMode = 0;
mManagedButton = 0;
FlexibleNumberPicker.this.removeCallbacks(this);
if (mIncrementVirtualButtonPressed) {
mIncrementVirtualButtonPressed = false;
invalidate();
}
mDecrementVirtualButtonPressed = false;
}
public void buttonPressDelayed(int button) {
cancel();
mMode = MODE_PRESS;
mManagedButton = button;
FlexibleNumberPicker.this.postDelayed(this, ViewConfiguration.getTapTimeout());
}
public void buttonTapped(int button) {
cancel();
mMode = MODE_TAPPED;
mManagedButton = button;
FlexibleNumberPicker.this.post(this);
}
@Override
public void run() {
switch (mMode) {
case MODE_PRESS: {
switch (mManagedButton) {
case BUTTON_INCREMENT: {
mIncrementVirtualButtonPressed = true;
invalidate();
} break;
case BUTTON_DECREMENT: {
mDecrementVirtualButtonPressed = true;
invalidate();
}
}
} break;
case MODE_TAPPED: {
switch (mManagedButton) {
case BUTTON_INCREMENT: {
if (!mIncrementVirtualButtonPressed) {
FlexibleNumberPicker.this.postDelayed(this,
ViewConfiguration.getPressedStateDuration());
}
mIncrementVirtualButtonPressed ^= true;
invalidate();
} break;
case BUTTON_DECREMENT: {
if (!mDecrementVirtualButtonPressed) {
FlexibleNumberPicker.this.postDelayed(this,
ViewConfiguration.getPressedStateDuration());
}
mDecrementVirtualButtonPressed ^= true;
invalidate();
}
}
} 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;
void setStep(boolean increment) {
mIncrement = increment;
}
@Override
public void run() {
changeValueByOne(mIncrement);
postDelayed(this, mLongPressUpdateInterval);
}
}
@SuppressLint("AppCompatCustomView")
public static class CustomEditText extends EditText {
public CustomEditText(Context context) {
super(context);
}
@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) {
int selectionDividerHeight = mSelectionDivider == null ? 0 : mSelectionDivider.getBounds().height();
switch (virtualViewId) {
case View.NO_ID:
return createAccessibilityNodeInfoForNumberPicker(getScrollX(), getScrollY(),
getScrollX() + getWidth(), getScrollY() + getHeight());
case VIRTUAL_VIEW_ID_DECREMENT:
return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT,
getVirtualDecrementButtonText(), getScrollX(), getScrollY(),
getScrollX() + getWidth(),
mTopSelectionDividerTop + selectionDividerHeight);
/*case VIRTUAL_VIEW_ID_INPUT:
return createAccessibiltyNodeInfoForInputText(getScrollX(),
mTopSelectionDividerTop + selectionDividerHeight,
getScrollX() + getWidth(),
mBottomSelectionDividerBottom - selectionDividerHeight);*/
case VIRTUAL_VIEW_ID_INCREMENT:
return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT,
getVirtualIncrementButtonText(), getScrollX(),
mBottomSelectionDividerBottom - selectionDividerHeight,
getScrollX() + getWidth(), getScrollY() + getHeight());
}
return super.createAccessibilityNodeInfo(virtualViewId);
}
@Override
public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched,
int virtualViewId) {
if (TextUtils.isEmpty(searched)) {
return Collections.emptyList();
}
String searchedLowerCase = searched.toLowerCase();
switch (virtualViewId) {
case View.NO_ID: {
List<AccessibilityNodeInfo> result = new ArrayList<>(3);
addIncDecNodeInfo(searchedLowerCase, result, getVirtualDecrementButtonText(), VIRTUAL_VIEW_ID_DECREMENT);
addInputNodeInfo(searchedLowerCase, result);
addIncDecNodeInfo(searchedLowerCase, result, getVirtualIncrementButtonText(), VIRTUAL_VIEW_ID_INCREMENT);
return result;
}
case VIRTUAL_VIEW_ID_DECREMENT:
case VIRTUAL_VIEW_ID_INCREMENT:
case VIRTUAL_VIEW_ID_INPUT: {
List<AccessibilityNodeInfo> result = new ArrayList<>(1);
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;
performAccessibilityAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
return true;
}
} return false;
case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
if (mAccessibilityFocusedView == virtualViewId) {
mAccessibilityFocusedView = UNDEFINED;
performAccessibilityAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
return true;
}
return false;
}
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
case android.R.id.accessibilityActionScrollDown: {
if (FlexibleNumberPicker.this.isEnabled()
&& (getWrapSelectorWheel() || getValue() < getMaxValue())) {
changeValueByOne(true);
return true;
}
} return false;
case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
case android.R.id.accessibilityActionScrollUp: {
if (FlexibleNumberPicker.this.isEnabled()
&& (getWrapSelectorWheel() || getValue() > getMinValue())) {
changeValueByOne(false);
return true;
}
} return false;
}
} break;
case VIRTUAL_VIEW_ID_INPUT: {
switch (action) {
/*case AccessibilityNodeInfo.ACTION_FOCUS: {
if (FlexibleNumberPicker.this.isEnabled() && !mInputText.isFocused()) {
return mInputText.requestFocus();
}
} break;*/
/*case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: {
if (FlexibleNumberPicker.this.isEnabled() && mInputText.isFocused()) {
mInputText.clearFocus();
return true;
}
return false;
}*/
case AccessibilityNodeInfo.ACTION_CLICK: {
if (FlexibleNumberPicker.this.isEnabled()) {
performClick();
return true;
}
return false;
}
case AccessibilityNodeInfo.ACTION_LONG_CLICK: {
if (FlexibleNumberPicker.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 (FlexibleNumberPicker.this.isEnabled()) {
FlexibleNumberPicker.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();
return true;
}
} return false;
case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
if (mAccessibilityFocusedView == virtualViewId) {
mAccessibilityFocusedView = UNDEFINED;
sendAccessibilityEventForVirtualView(virtualViewId,
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
invalidate();
return true;
}
} return false;
}
} return false;
case VIRTUAL_VIEW_ID_DECREMENT: {
switch (action) {
case AccessibilityNodeInfo.ACTION_CLICK: {
if (FlexibleNumberPicker.this.isEnabled()) {
FlexibleNumberPicker.this.changeValueByOne(false);
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();
return true;
}
} return false;
case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
if (mAccessibilityFocusedView == virtualViewId) {
mAccessibilityFocusedView = UNDEFINED;
sendAccessibilityEventForVirtualView(virtualViewId,
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
invalidate();
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().isEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
/*mInputText.onInitializeAccessibilityEvent(event);
mInputText.onPopulateAccessibilityEvent(event);*/
event.setSource(FlexibleNumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
requestSendAccessibilityEvent(FlexibleNumberPicker.this, event);
}
}
private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType,
String text) {
if (accessibilityManager().isEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
event.setClassName(Button.class.getName());
event.setPackageName(getContext().getPackageName());
event.getText().add(text);
event.setEnabled(FlexibleNumberPicker.this.isEnabled());
event.setSource(FlexibleNumberPicker.this, virtualViewId);
requestSendAccessibilityEvent(FlexibleNumberPicker.this, event);
}
}
private void findAccessibilityNodeInfosByTextInChild(String searchedLowerCase,
int virtualViewId, List<AccessibilityNodeInfo> outResult) {
switch (virtualViewId) {
case VIRTUAL_VIEW_ID_DECREMENT:
addIncDecNodeInfo(searchedLowerCase, outResult, getVirtualDecrementButtonText(), VIRTUAL_VIEW_ID_DECREMENT);
break;
case VIRTUAL_VIEW_ID_INPUT:
addInputNodeInfo(searchedLowerCase, outResult);
break;
case VIRTUAL_VIEW_ID_INCREMENT:
addIncDecNodeInfo(searchedLowerCase, outResult, getVirtualIncrementButtonText(), VIRTUAL_VIEW_ID_INCREMENT);
break;
}
}
private void addIncDecNodeInfo(
String searchedLowerCase, List<AccessibilityNodeInfo> outResult, String buttonText, int id) {
if (!TextUtils.isEmpty(buttonText) && buttonText.toLowerCase().contains(searchedLowerCase)) {
outResult.add(createAccessibilityNodeInfo(id));
}
}
private void addInputNodeInfo(String searchedLowerCase, List<AccessibilityNodeInfo> outResult) {
/*CharSequence text = mInputText.getText();
if (!TextUtils.isEmpty(text) &&
text.toString().toLowerCase().contains(searchedLowerCase)) {
outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
} else if (!TextUtils.isEmpty(text = mInputText.getContentDescription()) && // CHANGED from getText()
text.toString().toLowerCase().contains(searchedLowerCase)) {
outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
}*/
}
/*private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText(
int left, int top, int right, int bottom) {
AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo();
info.setSource(FlexibleNumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
info.setAccessibilityFocused(mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT);
if (mAccessibilityFocusedView != VIRTUAL_VIEW_ID_INPUT) {
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
}
if (mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT) {
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
}
setBounds(left, top, right, bottom, info);
return info;
}*/
private void setBounds(int left, int top, int right, int bottom, AccessibilityNodeInfo info) {
Rect boundsInScreen = mTempRect;
boundsInScreen.set(left, top, right, bottom);
info.setVisibleToUser(isVisibleToUser(boundsInScreen));
int[] locationOnScreen = mTempArray;
getLocationOnScreen(locationOnScreen);
boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
info.setBoundsInScreen(boundsInScreen);
}
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(getContext().getPackageName());
info.setSource(FlexibleNumberPicker.this, virtualViewId);
info.setParent(FlexibleNumberPicker.this);
info.setText(text);
info.setClickable(true);
info.setLongClickable(true);
info.setEnabled(FlexibleNumberPicker.this.isEnabled());
info.setAccessibilityFocused(mAccessibilityFocusedView == virtualViewId);
setBounds(left, top, right, bottom, info);
if (mAccessibilityFocusedView != virtualViewId) {
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
}
if (mAccessibilityFocusedView == virtualViewId) {
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
}
if (FlexibleNumberPicker.this.isEnabled()) {
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
}
return info;
}
private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top,
int right, int bottom) {
AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
info.setClassName(android.widget.NumberPicker.class.getName());
info.setPackageName(getContext().getPackageName());
info.setSource(FlexibleNumberPicker.this);
if (hasVirtualDecrementButton()) {
info.addChild(FlexibleNumberPicker.this, VIRTUAL_VIEW_ID_DECREMENT);
}
info.addChild(FlexibleNumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
if (hasVirtualIncrementButton()) {
info.addChild(FlexibleNumberPicker.this, VIRTUAL_VIEW_ID_INCREMENT);
}
info.setParent((View) getParentForAccessibility());
info.setEnabled(FlexibleNumberPicker.this.isEnabled());
info.setScrollable(true);
info.setAccessibilityFocused(mAccessibilityFocusedView == View.NO_ID);
// final float applicationScale = getContext().getResources().getCompatibilityInfo().applicationScale;
Rect boundsInScreen = mTempRect;
boundsInScreen.set(left, top, right, bottom);
// boundsInScreen.scale(applicationScale);
info.setVisibleToUser(isVisibleToUser(null));
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.AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
}
if (mAccessibilityFocusedView == View.NO_ID) {
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
}
if (FlexibleNumberPicker.this.isEnabled()) {
if (getWrapSelectorWheel() || getValue() < getMaxValue()) {
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN);
}
}
if (getWrapSelectorWheel() || getValue() > getMinValue()) {
info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
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);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment