NekoX/TMessagesProj/src/main/java/org/telegram/ui/Components/NumberPicker.java

1137 lines
43 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 org.telegram.ui.Components;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.text.TextUtils;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.DecelerateInterpolator;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.telegram.messenger.AndroidUtilities;
import org.telegram.ui.ActionBar.Theme;
import java.util.Locale;
public class NumberPicker extends LinearLayout {
private int SELECTOR_WHEEL_ITEM_COUNT = 3;
private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300;
private int SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2;
private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8;
private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;
private static final int SNAP_SCROLL_DURATION = 300;
private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f;
private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2;
private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48;
private static final int DEFAULT_LAYOUT_RESOURCE_ID = 0;
private static final int SIZE_UNSPECIFIED = -1;
private int textOffset;
private TextView mInputText;
private int mSelectionDividersDistance;
private int mMinHeight;
private int mMaxHeight;
private int mMinWidth;
private int mMaxWidth;
private boolean mComputeMaxWidth;
private int mTextSize;
private int mSelectorTextGapHeight;
private String[] mDisplayedValues;
private int mMinValue;
private int mMaxValue;
private int mValue;
private OnValueChangeListener mOnValueChangeListener;
private OnScrollListener mOnScrollListener;
private Formatter mFormatter;
private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL;
private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<>();
private int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT];
private Paint mSelectorWheelPaint;
private int mSelectorElementHeight;
private int mInitialScrollOffset = Integer.MIN_VALUE;
private int mCurrentScrollOffset;
private Scroller mFlingScroller;
private Scroller mAdjustScroller;
private int mPreviousScrollerY;
private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand;
private float mLastDownEventY;
private long mLastDownEventTime;
private float mLastDownOrMoveEventY;
private VelocityTracker mVelocityTracker;
private int mTouchSlop;
private int mMinimumFlingVelocity;
private int mMaximumFlingVelocity;
private boolean mWrapSelectorWheel;
private int mSolidColor;
private Paint mSelectionDivider;
private int mSelectionDividerHeight;
private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
private boolean mIngonreMoveEvents;
private int mTopSelectionDividerTop;
private int mBottomSelectionDividerBottom;
private int mLastHoveredChildVirtualViewId;
private boolean mIncrementVirtualButtonPressed;
private boolean mDecrementVirtualButtonPressed;
private PressedStateHelper mPressedStateHelper;
private int mLastHandledDownDpadKeyCode = -1;
private SeekBarAccessibilityDelegate accessibilityDelegate;
private boolean drawDividers = true;
public interface OnValueChangeListener {
void onValueChange(NumberPicker picker, int oldVal, int newVal);
}
public interface OnScrollListener {
int SCROLL_STATE_IDLE = 0;
int SCROLL_STATE_TOUCH_SCROLL = 1;
int SCROLL_STATE_FLING = 2;
void onScrollStateChange(NumberPicker view, int scrollState);
}
public interface Formatter {
String format(int value);
}
public void setItemCount(int count) {
if (SELECTOR_WHEEL_ITEM_COUNT == count) {
return;
}
SELECTOR_WHEEL_ITEM_COUNT = count;
SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2;
mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT];
initializeSelectorWheelIndices();
}
private void init() {
mSolidColor = 0;
mSelectionDivider = new Paint();
mSelectionDivider.setColor(Theme.getColor(Theme.key_dialogButton));
mSelectionDividerHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT, getResources().getDisplayMetrics());
mSelectionDividersDistance = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE, getResources().getDisplayMetrics());
mMinHeight = SIZE_UNSPECIFIED;
mMaxHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 180, getResources().getDisplayMetrics());
if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED && mMinHeight > mMaxHeight) {
throw new IllegalArgumentException("minHeight > maxHeight");
}
mMinWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 64, getResources().getDisplayMetrics());
mMaxWidth = SIZE_UNSPECIFIED;
if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED && mMinWidth > mMaxWidth) {
throw new IllegalArgumentException("minWidth > maxWidth");
}
mComputeMaxWidth = (mMaxWidth == SIZE_UNSPECIFIED);
mPressedStateHelper = new PressedStateHelper();
setWillNotDraw(false);
mInputText = new TextView(getContext());
mInputText.setGravity(Gravity.CENTER);
mInputText.setSingleLine(true);
mInputText.setTextColor(Theme.getColor(Theme.key_dialogTextBlack));
mInputText.setBackgroundResource(0);
mInputText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
mInputText.setVisibility(INVISIBLE);
addView(mInputText, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
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;
mFlingScroller = new Scroller(getContext(), null, true);
mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f));
updateInputTextView();
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
setAccessibilityDelegate(accessibilityDelegate = new SeekBarAccessibilityDelegate() {
@Override
protected void doScroll(View host, boolean backward) {
changeValueByOne(!backward);
}
@Override
protected boolean canScrollBackward(View host) {
return true;
}
@Override
protected boolean canScrollForward(View host) {
return true;
}
@Override
public CharSequence getContentDescription(View host) {
return NumberPicker.this.getContentDescription(mValue);
}
});
}
protected CharSequence getContentDescription(int value) {
return mInputText.getText();
}
public void setTextColor(int color) {
mInputText.setTextColor(color);
mSelectorWheelPaint.setColor(color);
}
public void setSelectorColor(int color) {
mSelectionDivider.setColor(color);
}
public NumberPicker(Context context) {
this(context, 18);
}
public NumberPicker(Context context, int textSize) {
super(context);
mTextSize = AndroidUtilities.dp(textSize);
init();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int msrdWdth = getMeasuredWidth();
final int msrdHght = getMeasuredHeight();
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) {
initializeSelectorWheel();
initializeFadingEdges();
mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2 - mSelectionDividerHeight;
mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight + mSelectionDividersDistance;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth);
final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight);
super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(), widthMeasureSpec);
final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(), heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
}
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();
if (action == MotionEvent.ACTION_DOWN) {
removeAllCallbacks();
mInputText.setVisibility(View.INVISIBLE);
mLastDownOrMoveEventY = mLastDownEventY = event.getY();
mLastDownEventTime = event.getEventTime();
mIngonreMoveEvents = false;
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);
}
}
getParent().requestDisallowInterceptTouchEvent(true);
if (!mFlingScroller.isFinished()) {
mFlingScroller.forceFinished(true);
mAdjustScroller.forceFinished(true);
onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
} else if (!mAdjustScroller.isFinished()) {
mFlingScroller.forceFinished(true);
mAdjustScroller.forceFinished(true);
} else if (mLastDownEventY < mTopSelectionDividerTop) {
postChangeCurrentByOneFromLongPress(false, ViewConfiguration.getLongPressTimeout());
} else if (mLastDownEventY > mBottomSelectionDividerBottom) {
postChangeCurrentByOneFromLongPress(true, ViewConfiguration.getLongPressTimeout());
}
return true;
}
return false;
}
public void finishScroll() {
if (!mFlingScroller.isFinished() || !mAdjustScroller.isFinished()) {
mFlingScroller.forceFinished(true);
mAdjustScroller.forceFinished(true);
mCurrentScrollOffset = mInitialScrollOffset;
invalidate();
}
}
@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 (mIngonreMoveEvents) {
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: {
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()) {
int selectorIndexOffset = (eventY / mSelectorElementHeight) - SELECTOR_MIDDLE_ITEM_INDEX;
if (selectorIndexOffset > 0) {
changeValueByOne(true);
mPressedStateHelper.buttonTapped(
PressedStateHelper.BUTTON_INCREMENT);
} else if (selectorIndexOffset < 0) {
changeValueByOne(false);
mPressedStateHelper.buttonTapped(
PressedStateHelper.BUTTON_DECREMENT);
}
} else {
ensureScrollWheelAdjusted();
}
onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
}
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
return true;
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
removeAllCallbacks();
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
final int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
removeAllCallbacks();
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_UP:
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
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;
if (!mWrapSelectorWheel && y > 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue && mCurrentScrollOffset + y > mInitialScrollOffset) {
mCurrentScrollOffset = mInitialScrollOffset;
return;
}
if (!mWrapSelectorWheel && y < 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue && mCurrentScrollOffset + y < mInitialScrollOffset) {
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) {
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) {
mCurrentScrollOffset = mInitialScrollOffset;
}
}
}
@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 setOnValueChangedListener(OnValueChangeListener onValueChangedListener) {
mOnValueChangeListener = onValueChangedListener;
}
public void setOnScrollListener(OnScrollListener onScrollListener) {
mOnScrollListener = onScrollListener;
}
public void setFormatter(Formatter formatter) {
if (formatter == mFormatter) {
return;
}
mFormatter = formatter;
initializeSelectorWheelIndices();
updateInputTextView();
}
public void setValue(int value) {
setValueInternal(value, false);
}
public void setTextOffset(int value) {
textOffset = value;
invalidate();
}
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 mDisplayedValue : mDisplayedValues) {
final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValue);
if (textWidth > maxTextWidth) {
maxTextWidth = (int) textWidth;
}
}
}
maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight();
if (mMaxWidth != maxTextWidth) {
if (maxTextWidth > mMinWidth) {
mMaxWidth = maxTextWidth;
} else {
mMaxWidth = mMinWidth;
}
invalidate();
}
}
public boolean getWrapSelectorWheel() {
return mWrapSelectorWheel;
}
public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length;
if ((!wrapSelectorWheel || wrappingAllowed) && wrapSelectorWheel != mWrapSelectorWheel) {
mWrapSelectorWheel = wrapSelectorWheel;
}
}
public void setOnLongPressUpdateInterval(long intervalMillis) {
mLongPressUpdateInterval = intervalMillis;
}
public int getValue() {
return mValue;
}
public int getMinValue() {
return mMinValue;
}
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;
}
boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
setWrapSelectorWheel(wrapSelectorWheel);
initializeSelectorWheelIndices();
updateInputTextView();
tryComputeMaxWidth();
invalidate();
}
public int getMaxValue() {
return mMaxValue;
}
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;
}
boolean wrapSelectorWheel = mMaxValue - mMinValue > mSelectorIndices.length;
setWrapSelectorWheel(wrapSelectorWheel);
initializeSelectorWheelIndices();
updateInputTextView();
tryComputeMaxWidth();
invalidate();
}
public String[] getDisplayedValues() {
return mDisplayedValues;
}
public void setDisplayedValues(String[] displayedValues) {
if (mDisplayedValues == displayedValues) {
return;
}
mDisplayedValues = displayedValues;
updateInputTextView();
initializeSelectorWheelIndices();
tryComputeMaxWidth();
}
@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();
}
@Override
protected void onDraw(Canvas canvas) {
float x = (getRight() - getLeft()) / 2 + textOffset;
float y = mCurrentScrollOffset;
// draw the selector wheel
int[] selectorIndices = mSelectorIndices;
for (int i = 0; i < selectorIndices.length; i++) {
int selectorIndex = selectorIndices[i];
String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
// Do not draw the middle item if input is visible since the input
// is shown only if the wheel is static and it covers the middle
// item. Otherwise, if the user starts editing the text via the
// IME he may see a dimmed version of the old value intermixed
// with the new one.
if (scrollSelectorValue != null && (i != SELECTOR_MIDDLE_ITEM_INDEX || mInputText.getVisibility() != VISIBLE)) {
canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint);
}
y += mSelectorElementHeight;
}
if (drawDividers) {
int topOfTopDivider = mTopSelectionDividerTop;
int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
canvas.drawRect(0, topOfTopDivider, getRight(), bottomOfTopDivider, mSelectionDivider);
int bottomOfBottomDivider = mBottomSelectionDividerBottom;
int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight;
canvas.drawRect(0, topOfBottomDivider, getRight(), bottomOfBottomDivider, mSelectionDivider);
}
}
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);
}
}
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;
}
}
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | 16777216;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result | (childMeasuredState & (-16777216));
}
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]);
}
}
private void setValueInternal(int current, boolean notifyChange) {
if (mValue == current) {
return;
}
if (mWrapSelectorWheel) {
current = getWrappedSelectorIndex(current);
} else {
current = Math.max(current, mMinValue);
current = Math.min(current, mMaxValue);
}
int previous = mValue;
mValue = current;
updateInputTextView();
if (notifyChange) {
notifyChange(previous, current);
}
initializeSelectorWheelIndices();
invalidate();
}
private void changeValueByOne(boolean increment) {
mInputText.setVisibility(View.INVISIBLE);
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();
}
private void initializeSelectorWheel() {
initializeSelectorWheelIndices();
int[] selectorIndices = mSelectorIndices;
int totalTextHeight = selectorIndices.length * mTextSize;
float totalTextGapHeight = (getBottom() - getTop()) - totalTextHeight;
float textGapCount = selectorIndices.length;
mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f);
mSelectorElementHeight = mTextSize + mSelectorTextGapHeight;
int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop();
mInitialScrollOffset = editTextTextPosition - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX);
mCurrentScrollOffset = mInitialScrollOffset;
updateInputTextView();
}
private void initializeFadingEdges() {
setVerticalFadingEdgeEnabled(true);
setFadingEdgeLength((getBottom() - getTop() - mTextSize) / 2);
}
private void onScrollerFinished(Scroller scroller) {
if (scroller == mFlingScroller) {
if (!ensureScrollWheelAdjusted()) {
updateInputTextView();
}
onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
} else {
if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
updateInputTextView();
}
}
}
private void onScrollStateChange(int scrollState) {
if (mScrollState == scrollState) {
return;
}
mScrollState = scrollState;
if (mOnScrollListener != null) {
mOnScrollListener.onScrollStateChange(this, scrollState);
}
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
AccessibilityManager am = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
if (am.isTouchExplorationEnabled()) {
String text = (mDisplayedValues == null) ? formatNumber(mValue) : mDisplayedValues[mValue - mMinValue];
AccessibilityEvent event = AccessibilityEvent.obtain();
event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
event.getText().add(text);
am.sendAccessibilityEvent(event);
}
}
}
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();
}
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;
}
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);
}
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);
}
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 boolean updateInputTextView() {
String text = (mDisplayedValues == null) ? formatNumber(mValue) : mDisplayedValues[mValue - mMinValue];
if (!TextUtils.isEmpty(text) && !text.equals(mInputText.getText().toString())) {
mInputText.setText(text);
return true;
}
return false;
}
private void notifyChange(int previous, int current) {
if (mOnValueChangeListener != null) {
mOnValueChangeListener.onValueChange(this, previous, mValue);
}
}
private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) {
if (mChangeCurrentByOneFromLongPressCommand == null) {
mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand();
} else {
removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
}
mChangeCurrentByOneFromLongPressCommand.setStep(increment);
postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis);
}
private void removeChangeCurrentByOneFromLongPress() {
if (mChangeCurrentByOneFromLongPressCommand != null) {
removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
}
}
private void removeAllCallbacks() {
if (mChangeCurrentByOneFromLongPressCommand != null) {
removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
}
mPressedStateHelper.cancel();
}
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;
}
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, getRight(), getBottom());
}
mDecrementVirtualButtonPressed = false;
if (mDecrementVirtualButtonPressed) {
invalidate(0, 0, getRight(), 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, getRight(), getBottom());
}
break;
case BUTTON_DECREMENT: {
mDecrementVirtualButtonPressed = true;
invalidate(0, 0, getRight(), mTopSelectionDividerTop);
}
}
}
break;
case MODE_TAPPED: {
switch (mManagedButton) {
case BUTTON_INCREMENT: {
if (!mIncrementVirtualButtonPressed) {
NumberPicker.this.postDelayed(this,
ViewConfiguration.getPressedStateDuration());
}
mIncrementVirtualButtonPressed ^= true;
invalidate(0, mBottomSelectionDividerBottom, getRight(), getBottom());
}
break;
case BUTTON_DECREMENT: {
if (!mDecrementVirtualButtonPressed) {
NumberPicker.this.postDelayed(this,
ViewConfiguration.getPressedStateDuration());
}
mDecrementVirtualButtonPressed ^= true;
invalidate(0, 0, getRight(), mTopSelectionDividerTop);
}
}
}
break;
}
}
}
class ChangeCurrentByOneFromLongPressCommand implements Runnable {
private boolean mIncrement;
private void setStep(boolean increment) {
mIncrement = increment;
}
@Override
public void run() {
changeValueByOne(mIncrement);
postDelayed(this, mLongPressUpdateInterval);
}
}
static private String formatNumberWithLocale(int value) {
return String.format(Locale.getDefault(), "%d", value);
}
public void setDrawDividers(boolean drawDividers) {
this.drawDividers = drawDividers;
invalidate();
}
}