Java tutorial
/* * 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.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.support.v4.content.ContextCompat; import android.text.TextUtils; import android.util.AttributeSet; 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.animation.DecelerateInterpolator; import android.widget.LinearLayout; import android.widget.TextView; import org.telegram.messenger.R; import java.util.Locale; public class NumberPicker extends LinearLayout { private static final int SELECTOR_WHEEL_ITEM_COUNT = 3; private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300; private static final 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 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 final int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT]; private Paint mSelectorWheelPaint; private Drawable mVirtualButtonPressedDrawable; 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 Drawable 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; 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); } private void init() { mSolidColor = 0; mSelectionDivider = new ColorDrawable(ContextCompat.getColor(getContext(), R.color.colorAccent) & 0x00ffffff | (0x9f << 24)) /*getResources().getDrawable(R.drawable.numberpicker_selection_divider)*/; 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); mVirtualButtonPressedDrawable = getResources().getDrawable(R.drawable.item_background_holo_light); mPressedStateHelper = new PressedStateHelper(); setWillNotDraw(false); mInputText = new TextView(getContext()); addView(mInputText); mInputText.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); mInputText.setGravity(Gravity.CENTER); mInputText.setSingleLine(true); mInputText.setBackgroundResource(0); mInputText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 18); ViewConfiguration configuration = ViewConfiguration.get(getContext()); mTouchSlop = configuration.getScaledTouchSlop(); mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT; mTextSize = (int) mInputText.getTextSize(); 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(); } public NumberPicker(Context context) { super(context); init(); } public NumberPicker(Context context, AttributeSet attrs) { super(context, attrs); init(); } public NumberPicker(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); 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(); switch (action) { case 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; } @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 = mInitialScrollOffset; return; } if (!mWrapSelectorWheel && y < 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { mCurrentScrollOffset = mInitialScrollOffset; return; } mCurrentScrollOffset += y; while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) { mCurrentScrollOffset -= mSelectorElementHeight; decrementSelectorIndices(selectorIndices); setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) { mCurrentScrollOffset = mInitialScrollOffset; } } while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) { mCurrentScrollOffset += mSelectorElementHeight; incrementSelectorIndices(selectorIndices); setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true); if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) { mCurrentScrollOffset = mInitialScrollOffset; } } } @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); } 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; float y = mCurrentScrollOffset; if (mVirtualButtonPressedDrawable != null && mScrollState == OnScrollListener.SCROLL_STATE_IDLE) { if (mDecrementVirtualButtonPressed) { mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET); mVirtualButtonPressedDrawable.setBounds(0, 0, getRight(), mTopSelectionDividerTop); mVirtualButtonPressedDrawable.draw(canvas); } if (mIncrementVirtualButtonPressed) { mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET); mVirtualButtonPressedDrawable.setBounds(0, mBottomSelectionDividerBottom, getRight(), getBottom()); mVirtualButtonPressedDrawable.draw(canvas); } } // draw the selector wheel int[] selectorIndices = mSelectorIndices; for (int i = 0; i < selectorIndices.length; i++) { int selectorIndex = selectorIndices[i]; String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); // Do not draw the middle item if input is visible since the input // is shown only if the wheel is static and it covers the middle // item. Otherwise, if the user starts editing the text via the // IME he may see a dimmed version of the old value intermixed // with the new one. if (i != SELECTOR_MIDDLE_ITEM_INDEX || mInputText.getVisibility() != VISIBLE) { canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint); } y += mSelectorElementHeight; } // draw the selection dividers if (mSelectionDivider != null) { // draw the top divider int topOfTopDivider = mTopSelectionDividerTop; int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight; mSelectionDivider.setBounds(0, topOfTopDivider, getRight(), bottomOfTopDivider); mSelectionDivider.draw(canvas); // draw the bottom divider int bottomOfBottomDivider = mBottomSelectionDividerBottom; int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight; mSelectionDivider.setBounds(0, topOfBottomDivider, getRight(), bottomOfBottomDivider); mSelectionDivider.draw(canvas); } } 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); } } 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); } }