diff options
-rw-r--r-- | core/java/android/app/TimePickerDialog.java | 32 | ||||
-rw-r--r-- | core/java/android/widget/NumberPicker.java | 1380 | ||||
-rw-r--r-- | core/java/android/widget/NumberPickerButton.java | 96 | ||||
-rw-r--r-- | core/java/android/widget/TimePicker.java | 94 | ||||
-rw-r--r-- | core/res/res/drawable/timepicker_down_btn.xml | 31 | ||||
-rw-r--r-- | core/res/res/drawable/timepicker_down_btn_holo_dark.xml | 43 | ||||
-rw-r--r-- | core/res/res/drawable/timepicker_down_btn_holo_light.xml | 43 | ||||
-rw-r--r-- | core/res/res/drawable/timepicker_input.xml | 31 | ||||
-rw-r--r-- | core/res/res/drawable/timepicker_up_btn.xml | 31 | ||||
-rw-r--r-- | core/res/res/drawable/timepicker_up_btn_holo_dark.xml | 43 | ||||
-rw-r--r-- | core/res/res/drawable/timepicker_up_btn_holo_light.xml | 43 | ||||
-rw-r--r-- | core/res/res/layout/number_picker.xml | 21 | ||||
-rw-r--r-- | core/res/res/layout/time_picker.xml | 40 | ||||
-rwxr-xr-x | core/res/res/values/attrs.xml | 15 | ||||
-rw-r--r-- | core/res/res/values/donottranslate.xml | 2 | ||||
-rwxr-xr-x | core/res/res/values/strings.xml | 4 | ||||
-rw-r--r-- | core/res/res/values/styles.xml | 61 | ||||
-rw-r--r-- | core/res/res/values/themes.xml | 20 |
18 files changed, 1545 insertions, 485 deletions
diff --git a/core/java/android/app/TimePickerDialog.java b/core/java/android/app/TimePickerDialog.java index 381143c..26af4eb 100644 --- a/core/java/android/app/TimePickerDialog.java +++ b/core/java/android/app/TimePickerDialog.java @@ -16,29 +16,26 @@ package android.app; +import com.android.internal.R; + import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.os.Build; import android.os.Bundle; -import android.text.format.DateFormat; import android.view.LayoutInflater; import android.view.View; import android.widget.TimePicker; import android.widget.TimePicker.OnTimeChangedListener; -import com.android.internal.R; - -import java.util.Calendar; - /** * A dialog that prompts the user for the time of day using a {@link TimePicker}. * * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-timepicker.html">Time Picker * tutorial</a>.</p> */ -public class TimePickerDialog extends AlertDialog implements OnClickListener, - OnTimeChangedListener { +public class TimePickerDialog extends AlertDialog + implements OnClickListener, OnTimeChangedListener { /** * The callback interface used to indicate the user is done filling in @@ -60,8 +57,6 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, private final TimePicker mTimePicker; private final OnTimeSetListener mCallback; - private final Calendar mCalendar; - private final java.text.DateFormat mDateFormat; int mInitialHourOfDay; int mInitialMinute; @@ -102,14 +97,13 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, mInitialMinute = minute; mIs24HourView = is24HourView; - mDateFormat = DateFormat.getTimeFormat(context); - mCalendar = Calendar.getInstance(); - updateTitle(mInitialHourOfDay, mInitialMinute); + setCanceledOnTouchOutside(false); + setIcon(0); + setTitle(R.string.time_picker_dialog_title); setButton(BUTTON_POSITIVE, context.getText(R.string.date_time_set), this); setButton(BUTTON_NEGATIVE, context.getText(R.string.cancel), (OnClickListener) null); - setIcon(R.drawable.ic_dialog_time); LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -132,19 +126,13 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, } } - public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { - updateTitle(hourOfDay, minute); - } - public void updateTime(int hourOfDay, int minutOfHour) { mTimePicker.setCurrentHour(hourOfDay); mTimePicker.setCurrentMinute(minutOfHour); } - private void updateTitle(int hour, int minute) { - mCalendar.set(Calendar.HOUR_OF_DAY, hour); - mCalendar.set(Calendar.MINUTE, minute); - setTitle(mDateFormat.format(mCalendar.getTime())); + public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { + /* do nothing */ } @Override @@ -164,7 +152,5 @@ public class TimePickerDialog extends AlertDialog implements OnClickListener, mTimePicker.setCurrentHour(hour); mTimePicker.setCurrentMinute(minute); mTimePicker.setIs24HourView(savedInstanceState.getBoolean(IS_24_HOUR)); - mTimePicker.setOnTimeChangedListener(this); - updateTitle(hour, minute); } } diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index 4482b5b..0c298b0 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -18,80 +18,134 @@ package android.widget; import com.android.internal.R; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.annotation.Widget; import android.content.Context; -import android.os.Handler; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Paint.Align; import android.text.InputFilter; import android.text.InputType; import android.text.Spanned; +import android.text.TextUtils; import android.text.method.NumberKeyListener; import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.VelocityTracker; import android.view.View; +import android.view.ViewConfiguration; +import android.view.LayoutInflater.Filter; +import android.view.animation.OvershootInterpolator; +import android.view.inputmethod.InputMethodManager; /** - * A view for selecting a number + * A view for selecting a number For a dialog using this view, see + * {@link android.app.TimePickerDialog}. * - * For a dialog using this view, see {@link android.app.TimePickerDialog}. * @hide */ @Widget public class NumberPicker extends LinearLayout { /** - * The callback interface used to indicate the number value has been adjusted. + * The index of the middle selector item. */ - public interface OnChangedListener { - /** - * @param picker The NumberPicker associated with this listener. - * @param oldVal The previous value. - * @param newVal The new value. - */ - void onChanged(NumberPicker picker, int oldVal, int newVal); - } + private static final int SELECTOR_MIDDLE_ITEM_INDEX = 2; /** - * Interface used to format the number into a string for presentation + * The coefficient by which to adjust (divide) the max fling velocity. */ - public interface Formatter { - String toString(int value); - } + private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8; - /* - * Use a custom NumberPicker formatting callback to use two-digit - * minutes strings like "01". Keeping a static formatter etc. is the - * most efficient way to do this; it avoids creating temporary objects - * on every call to format(). - */ - public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = - new NumberPicker.Formatter() { - final StringBuilder mBuilder = new StringBuilder(); - final java.util.Formatter mFmt = new java.util.Formatter(mBuilder); - final Object[] mArgs = new Object[1]; - public String toString(int value) { - mArgs[0] = value; - mBuilder.delete(0, mBuilder.length()); - mFmt.format("%02d", mArgs); - return mFmt.toString(); - } - }; + /** + * The the duration for adjusting the selector wheel. + */ + private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800; - private final Handler mHandler; - private final Runnable mRunnable = new Runnable() { - public void run() { - if (mIncrement) { - changeCurrent(mCurrent + 1); - mHandler.postDelayed(this, mSpeed); - } else if (mDecrement) { - changeCurrent(mCurrent - 1); - mHandler.postDelayed(this, mSpeed); - } + /** + * The the delay for showing the input controls after a single tap on the + * input text. + */ + private static final int SHOW_INPUT_CONTROLS_DELAY_MILLIS = ViewConfiguration + .getDoubleTapTimeout(); + + /** + * The update step for incrementing the current value. + */ + private static final int UPDATE_STEP_INCREMENT = 1; + + /** + * The update step for decrementing the current value. + */ + private static final int UPDATE_STEP_DECREMENT = -1; + + /** + * The strength of fading in the top and bottom while drawing the selector. + */ + private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f; + + /** + * The numbers accepted by the input text's {@link Filter} + */ + private static final char[] DIGIT_CHARACTERS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' + }; + + /** + * Use a custom NumberPicker formatting callback to use two-digit minutes + * strings like "01". Keeping a static formatter etc. is the most efficient + * way to do this; it avoids creating temporary objects on every call to + * format(). + */ + public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = new NumberPicker.Formatter() { + final StringBuilder mBuilder = new StringBuilder(); + + final java.util.Formatter mFmt = new java.util.Formatter(mBuilder); + + final Object[] mArgs = new Object[1]; + + public String toString(int value) { + mArgs[0] = value; + mBuilder.delete(0, mBuilder.length()); + mFmt.format("%02d", mArgs); + return mFmt.toString(); } }; - private final EditText mText; - private final InputFilter mNumberInputFilter; + /** + * The increment button. + */ + private final ImageButton mIncrementButton; + + /** + * The decrement button. + */ + private final ImageButton mDecrementButton; + + /** + * The text for showing the current value. + */ + private final EditText mInputText; + + /** + * The height of the text. + */ + private final int mTextSize; + /** + * The values to be displayed instead the indices. + */ private String[] mDisplayedValues; /** @@ -110,104 +164,466 @@ public class NumberPicker extends LinearLayout { private int mCurrent; /** - * Previous value of this NumberPicker. + * Listener to be notified upon current value change. */ - private int mPrevious; private OnChangedListener mListener; + + /** + * Formatter for for displaying the current value. + */ private Formatter mFormatter; - private long mSpeed = 300; - private boolean mIncrement; - private boolean mDecrement; + /** + * The speed for updating the value form long press. + */ + private long mLongPressUpdateSpeed = 300; /** - * Create a new number picker - * @param context the application environment + * Cache for the string representation of selector indices. + */ + private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>(); + + /** + * The selector indices whose value are show by the selector. + */ + private final int[] mSelectorIndices = new int[] { + Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, + Integer.MIN_VALUE + }; + + /** + * The {@link Paint} for drawing the selector. + */ + private final Paint mSelectorPaint; + + /** + * 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. + */ + private SetSelectionCommand mSetSelectionCommand; + + /** + * Handle to the reusable command for adjusting the scroller. + */ + private AdjustScrollerCommand mAdjustScrollerCommand; + + /** + * Handle to the reusable command for updating the current value from long + * press. + */ + private UpdateValueFromLongPressCommand mUpdateFromLongPressCommand; + + /** + * {@link Animator} for showing the up/down arrows. + */ + private final AnimatorSet mShowInputControlsAnimator; + + /** + * The Y position of the last down event. + */ + private float mLastDownEventY; + + /** + * The Y position of the last motion event. + */ + private float mLastMotionEventY; + + /** + * Flag if to begin edit on next up event. + */ + private boolean mBeginEditOnUpEvent; + + /** + * Flag if to adjust the selector wheel on next up event. + */ + private boolean mAdjustScrollerOnUpEvent; + + /** + * Flag if to draw the selector wheel. + */ + private boolean mDrawSelectorWheel; + + /** + * Determines speed during touch scrolling. + */ + private VelocityTracker mVelocityTracker; + + /** + * @see ViewConfiguration#getScaledTouchSlop() + */ + private int mTouchSlop; + + /** + * @see ViewConfiguration#getScaledMinimumFlingVelocity() + */ + private int mMinimumFlingVelocity; + + /** + * @see ViewConfiguration#getScaledMaximumFlingVelocity() + */ + private int mMaximumFlingVelocity; + + /** + * Flag whether the selector should wrap around. + */ + private boolean mWrapSelector; + + /** + * The back ground color used to optimize scroller fading. */ - public NumberPicker(Context context) { - this(context, null); + private final int mSolidColor; + + /** + * Reusable {@link Rect} instance. + */ + private final Rect mTempRect = new Rect(); + + /** + * The callback interface used to indicate the number value has changed. + */ + public interface OnChangedListener { + /** + * @param picker The NumberPicker associated with this listener. + * @param oldVal The previous value. + * @param newVal The new value. + */ + void onChanged(NumberPicker picker, int oldVal, int newVal); + } + + /** + * Interface used to format the number into a string for presentation + */ + public interface Formatter { + String toString(int value); } /** * Create a new number picker - * @param context the application environment - * @param attrs a collection of attributes + * + * @param context The application environment. + * @param attrs A collection of attributes. */ public NumberPicker(Context context, AttributeSet attrs) { - super(context, attrs); - setOrientation(VERTICAL); - LayoutInflater inflater = - (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + this(context, attrs, R.attr.numberPickerStyle); + } + + /** + * Create a new number picker + * + * @param context the application environment. + * @param attrs a collection of attributes. + * @param defStyle The default style to apply to this view. + */ + public NumberPicker(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // process style attributes + TypedArray attributesArray = context.obtainStyledAttributes(attrs, + R.styleable.NumberPicker, defStyle, 0); + int orientation = attributesArray.getInt(R.styleable.NumberPicker_orientation, VERTICAL); + setOrientation(orientation); + mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0); + attributesArray.recycle(); + + // 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); + setDrawSelectorWheel(false); + + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.number_picker, this, true); - mHandler = new Handler(); - OnClickListener clickListener = new OnClickListener() { + OnClickListener onClickListener = new OnClickListener() { public void onClick(View v) { - validateInput(mText); - if (!mText.hasFocus()) mText.requestFocus(); - - // now perform the increment/decrement - if (R.id.increment == v.getId()) { + mInputText.clearFocus(); + if (v.getId() == R.id.increment) { changeCurrent(mCurrent + 1); - } else if (R.id.decrement == v.getId()) { + } else { changeCurrent(mCurrent - 1); } } }; - OnFocusChangeListener focusListener = new OnFocusChangeListener() { - public void onFocusChange(View v, boolean hasFocus) { + OnLongClickListener onLongClickListener = new OnLongClickListener() { + public boolean onLongClick(View v) { + mInputText.clearFocus(); + if (v.getId() == R.id.increment) { + postUpdateValueFromLongPress(UPDATE_STEP_INCREMENT); + } else { + postUpdateValueFromLongPress(UPDATE_STEP_DECREMENT); + } + return true; + } + }; - /* When focus is lost check that the text field - * has valid values. - */ + // increment button + mIncrementButton = (ImageButton) findViewById(R.id.increment); + mIncrementButton.setOnClickListener(onClickListener); + mIncrementButton.setOnLongClickListener(onLongClickListener); + + // decrement button + mDecrementButton = (ImageButton) findViewById(R.id.decrement); + mDecrementButton.setOnClickListener(onClickListener); + mDecrementButton.setOnLongClickListener(onLongClickListener); + + // input text + mInputText = (EditText) findViewById(R.id.timepicker_input); + mInputText.setOnFocusChangeListener(new OnFocusChangeListener() { + public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus) { - validateInput(v); + validateInputTextView(v); } } - }; + }); + mInputText.setFilters(new InputFilter[] { + new InputTextFilter() + }); - OnLongClickListener longClickListener = new OnLongClickListener() { - /** - * We start the long click here but rely on the {@link NumberPickerButton} - * to inform us when the long click has ended. - */ - public boolean onLongClick(View v) { - /* The text view may still have focus so clear it's focus which will - * trigger the on focus changed and any typed values to be pulled. - */ - mText.clearFocus(); - - if (R.id.increment == v.getId()) { - mIncrement = true; - mHandler.post(mRunnable); - } else if (R.id.decrement == v.getId()) { - mDecrement = true; - mHandler.post(mRunnable); + mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); + + // initialize constants + mTouchSlop = ViewConfiguration.getTapTimeout(); + ViewConfiguration configuration = ViewConfiguration.get(context); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity() + / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT; + mTextSize = (int) mInputText.getTextSize(); + + // create the selector wheel paint + Paint paint = new Paint(); + paint.setAntiAlias(true); + paint.setTextAlign(Align.CENTER); + paint.setTextSize(mTextSize); + paint.setTypeface(mInputText.getTypeface()); + ColorStateList colors = mInputText.getTextColors(); + int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE); + paint.setColor(color); + mSelectorPaint = paint; + + // create the animator for showing the input controls + final ValueAnimator fadeScroller = ObjectAnimator.ofInt(this, "selectorPaintAlpha", 255, 0); + final ObjectAnimator showIncrementButton = ObjectAnimator.ofFloat(mIncrementButton, + "alpha", 0, 1); + final ObjectAnimator showDecrementButton = ObjectAnimator.ofFloat(mDecrementButton, + "alpha", 0, 1); + mShowInputControlsAnimator = new AnimatorSet(); + mShowInputControlsAnimator.playTogether(fadeScroller, showIncrementButton, + showDecrementButton); + mShowInputControlsAnimator.setDuration(getResources().getInteger( + R.integer.config_longAnimTime)); + mShowInputControlsAnimator.addListener(new AnimatorListenerAdapter() { + private boolean mCanceled = false; + + @Override + public void onAnimationEnd(Animator animation) { + if (!mCanceled) { + // if canceled => we still want the wheel drawn + setDrawSelectorWheel(false); } - return true; + mCanceled = false; + mSelectorPaint.setAlpha(255); + invalidate(); } - }; - InputFilter inputFilter = new NumberPickerInputFilter(); - mNumberInputFilter = new NumberRangeKeyListener(); - mIncrementButton = (NumberPickerButton) findViewById(R.id.increment); - mIncrementButton.setOnClickListener(clickListener); - mIncrementButton.setOnLongClickListener(longClickListener); - mIncrementButton.setNumberPicker(this); + @Override + public void onAnimationCancel(Animator animation) { + if (mShowInputControlsAnimator.isRunning()) { + mCanceled = true; + } + } + }); - mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement); - mDecrementButton.setOnClickListener(clickListener); - mDecrementButton.setOnLongClickListener(longClickListener); - mDecrementButton.setNumberPicker(this); + // create the fling and adjust scrollers + mFlingScroller = new Scroller(getContext()); + mAdjustScroller = new Scroller(getContext(), new OvershootInterpolator()); - mText = (EditText) findViewById(R.id.timepicker_input); - mText.setOnFocusChangeListener(focusListener); - mText.setFilters(new InputFilter[] {inputFilter}); - mText.setRawInputType(InputType.TYPE_CLASS_NUMBER); + updateInputTextView(); + updateIncrementAndDecrementButtonsVisibilityState(); + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + if (!hasWindowFocus) { + removeAllCallbacks(); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mLastMotionEventY = mLastDownEventY = event.getY(); + removeAllCallbacks(); + mBeginEditOnUpEvent = false; + mAdjustScrollerOnUpEvent = true; + if (mDrawSelectorWheel) { + mBeginEditOnUpEvent = mFlingScroller.isFinished() + && mAdjustScroller.isFinished(); + mAdjustScrollerOnUpEvent = true; + mFlingScroller.forceFinished(true); + mAdjustScroller.forceFinished(true); + hideInputControls(); + return true; + } + if (isEventInInputText(event)) { + mAdjustScrollerOnUpEvent = false; + setDrawSelectorWheel(true); + hideInputControls(); + return true; + } + break; + case MotionEvent.ACTION_MOVE: + float currentMoveY = event.getY(); + int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); + if (deltaDownY > mTouchSlop) { + mBeginEditOnUpEvent = false; + setDrawSelectorWheel(true); + hideInputControls(); + return true; + } + break; + } + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + int action = ev.getActionMasked(); + switch (action) { + case MotionEvent.ACTION_MOVE: + float currentMoveY = ev.getY(); + if (mBeginEditOnUpEvent) { + int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY); + if (deltaDownY > mTouchSlop) { + mBeginEditOnUpEvent = false; + } + } + int deltaMoveY = (int) (currentMoveY - mLastMotionEventY); + scrollBy(0, deltaMoveY); + invalidate(); + mLastMotionEventY = currentMoveY; + break; + case MotionEvent.ACTION_UP: + if (mBeginEditOnUpEvent) { + setDrawSelectorWheel(false); + showInputControls(); + mInputText.requestFocus(); + InputMethodManager imm = (InputMethodManager) getContext().getSystemService( + Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mInputText, 0); + return true; + } + VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); + int initialVelocity = (int) velocityTracker.getYVelocity(); + if (Math.abs(initialVelocity) > mMinimumFlingVelocity) { + fling(initialVelocity); + } else { + if (mAdjustScrollerOnUpEvent) { + if (mFlingScroller.isFinished() && mAdjustScroller.isFinished()) { + postAdjustScrollerCommand(0); + } + } else { + postAdjustScrollerCommand(SHOW_INPUT_CONTROLS_DELAY_MILLIS); + } + } + mVelocityTracker.recycle(); + mVelocityTracker = null; + break; + } + return true; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + int action = event.getActionMasked(); + if ((action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) + && !isEventInInputText(event)) { + removeAllCallbacks(); + } + return super.dispatchTouchEvent(event); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + int keyCode = event.getKeyCode(); + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) { + removeAllCallbacks(); + } + return super.dispatchKeyEvent(event); + } + + @Override + public boolean dispatchTrackballEvent(MotionEvent event) { + int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + removeAllCallbacks(); + } + return super.dispatchTrackballEvent(event); + } - if (!isEnabled()) { - setEnabled(false); + @Override + public void computeScroll() { + if (!mDrawSelectorWheel) { + return; + } + 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(); } } @@ -222,11 +638,57 @@ public class NumberPicker extends LinearLayout { super.setEnabled(enabled); mIncrementButton.setEnabled(enabled); mDecrementButton.setEnabled(enabled); - mText.setEnabled(enabled); + mInputText.setEnabled(enabled); + } + + /** + * Scrolls the selector with the given <code>vertical offset</code>. + */ + @Override + public void scrollBy(int x, int y) { + int[] selectorIndices = getSelectorIndices(); + if (mInitialScrollOffset == Integer.MIN_VALUE) { + int totalTextHeight = selectorIndices.length * mTextSize; + int totalTextGapHeight = (mBottom - mTop) - totalTextHeight; + int textGapCount = selectorIndices.length - 1; + int selectorTextGapHeight = totalTextGapHeight / textGapCount; + // compensate for integer division loss of the components used to + // calculate the text gap + int integerDivisionLoss = (mTextSize + mBottom - mTop) % textGapCount; + mInitialScrollOffset = mCurrentScrollOffset = mTextSize - integerDivisionLoss / 2; + mSelectorElementHeight = mTextSize + selectorTextGapHeight; + } + + if (!mWrapSelector && y > 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mStart) { + mCurrentScrollOffset = mInitialScrollOffset; + return; + } + if (!mWrapSelector && y < 0 && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mEnd) { + mCurrentScrollOffset = mInitialScrollOffset; + return; + } + mCurrentScrollOffset += y; + while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorElementHeight) { + mCurrentScrollOffset -= mSelectorElementHeight; + decrementSelectorIndices(selectorIndices); + changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); + if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mStart) { + mCurrentScrollOffset = mInitialScrollOffset; + } + } + while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorElementHeight) { + mCurrentScrollOffset += mSelectorElementHeight; + incrementScrollSelectorIndices(selectorIndices); + changeCurrent(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX]); + if (selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mEnd) { + mCurrentScrollOffset = mInitialScrollOffset; + } + } } /** * Set the callback that indicates the number has been adjusted by the user. + * * @param listener the callback, should not be null. */ public void setOnChangeListener(OnChangedListener listener) { @@ -235,292 +697,678 @@ public class NumberPicker extends LinearLayout { /** * Set the formatter that will be used to format the number for presentation - * @param formatter the formatter object. If formatter is null, String.valueOf() - * will be used + * + * @param formatter the formatter object. If formatter is null, + * String.valueOf() will be used */ public void setFormatter(Formatter formatter) { mFormatter = formatter; } /** - * Set the range of numbers allowed for the number picker. The current - * value will be automatically set to the start. + * Set the range of numbers allowed for the number picker. The current value + * will be automatically set to the start. * * @param start the start of the range (inclusive) * @param end the end of the range (inclusive) */ public void setRange(int start, int end) { - setRange(start, end, null/*displayedValues*/); + setRange(start, end, null); } /** - * Set the range of numbers allowed for the number picker. The current - * value will be automatically set to the start. Also provide a mapping - * for values used to display to the user. + * Set the range of numbers allowed for the number picker. The current value + * will be automatically set to the start. Also provide a mapping for values + * used to display to the user. * * @param start the start of the range (inclusive) * @param end the end of the range (inclusive) * @param displayedValues the values displayed to the user. */ public void setRange(int start, int end, String[] displayedValues) { + boolean wrapSelector = (end - start) >= mSelectorIndices.length; + setRange(start, end, displayedValues, wrapSelector); + } + + /** + * Set the range of numbers allowed for the number picker. The current value + * will be automatically set to the start. Also provide a mapping for values + * used to display to the user. + * + * @param start the start of the range (inclusive) + * @param end the end of the range (inclusive) + * @param displayedValues the values displayed to the user. + */ + public void setRange(int start, int end, String[] displayedValues, boolean wrapSelector) { + if (start < 0 || end < 0) { + throw new IllegalArgumentException("start and end must be > 0"); + } + mDisplayedValues = displayedValues; mStart = start; mEnd = end; mCurrent = start; - updateView(); + + setWrapSelector(wrapSelector); + updateInputTextView(); if (displayedValues != null) { // Allow text entry rather than strictly numeric entry. - mText.setRawInputType(InputType.TYPE_CLASS_TEXT | - InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + } else { + mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER); } + + // make sure cached string representations are dropped + mSelectorIndexToStringCache.clear(); } /** * Set the current value for the number picker. * * @param current the current value the start of the range (inclusive) - * @throws IllegalArgumentException when current is not within the range - * of of the number picker + * @throws IllegalArgumentException when current is not within the range of + * of the number picker */ public void setCurrent(int current) { if (current < mStart || current > mEnd) { - throw new IllegalArgumentException( - "current should be >= start and <= end"); + throw new IllegalArgumentException("current should be >= start and <= end"); } mCurrent = current; - updateView(); + updateInputTextView(); + updateIncrementAndDecrementButtonsVisibilityState(); + } + + /** + * Sets whether the selector shown during flinging/scrolling should wrap + * around the beginning and end values. + * + * @param wrapSelector Whether to wrap. + */ + public void setWrapSelector(boolean wrapSelector) { + if (wrapSelector && (mEnd - mStart) < mSelectorIndices.length) { + throw new IllegalStateException("Range less than selector items count."); + } + if (wrapSelector != mWrapSelector) { + // force the selector indices array to be reinitialized + mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] = Integer.MAX_VALUE; + mWrapSelector = wrapSelector; + } } /** - * Sets the speed at which the numbers will scroll when the +/- - * buttons are longpressed + * Sets the speed at which the numbers will scroll when the +/- buttons are + * longpressed * * @param speed The speed (in milliseconds) at which the numbers will scroll - * default 300ms + * default 300ms */ public void setSpeed(long speed) { - mSpeed = speed; + mLongPressUpdateSpeed = speed; } - private String formatNumber(int value) { - return (mFormatter != null) - ? mFormatter.toString(value) - : String.valueOf(value); + /** + * Returns the current value of the NumberPicker + * + * @return the current value. + */ + public int getCurrent() { + return mCurrent; + } + + @Override + public int getSolidColor() { + return mSolidColor; + } + + @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() { + removeAllCallbacks(); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + // There is a good reason for doing this. See comments in draw(). + } + + @Override + public void draw(Canvas canvas) { + // Dispatch draw to our children only if we are not currently running + // the animation for simultaneously fading out the scroll wheel and + // showing in the buttons. This class takes advantage of the View + // implementation of fading edges effect to draw the selector wheel. + // However, in View.draw(), the fading is applied after all the children + // have been drawn and we do not want this fading to be applied to the + // buttons which are currently showing in. Therefore, we draw our + // children + // after we have completed drawing ourselves. + + // Draw the selector wheel if needed + if (mDrawSelectorWheel) { + super.draw(canvas); + } + + // Draw our children if we are not showing the selector wheel of fading + // it out + if (mShowInputControlsAnimator.isRunning() || !mDrawSelectorWheel) { + long drawTime = getDrawingTime(); + for (int i = 0, count = getChildCount(); i < count; i++) { + View child = getChildAt(i); + if (!child.isShown()) { + continue; + } + drawChild(canvas, getChildAt(i), drawTime); + } + } + } + + @Override + protected void onDraw(Canvas canvas) { + // we only draw the selector wheel + if (!mDrawSelectorWheel) { + return; + } + float x = (mRight - mLeft) / 2; + float y = mCurrentScrollOffset; + + int[] selectorIndices = getSelectorIndices(); + for (int i = 0; i < selectorIndices.length; i++) { + int selectorIndex = selectorIndices[i]; + String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex); + canvas.drawText(scrollSelectorValue, x, y, mSelectorPaint); + y += mSelectorElementHeight; + } } /** - * Sets the current value of this NumberPicker, and sets mPrevious to the previous - * value. If current is greater than mEnd less than mStart, the value of mCurrent - * is wrapped around. + * Returns the upper value of the range of the NumberPicker * - * Subclasses can override this to change the wrapping behavior + * @return the uppper number of the range. + */ + protected int getEndRange() { + return mEnd; + } + + /** + * Returns the lower value of the range of the NumberPicker + * + * @return the lower number of the range. + */ + protected int getBeginRange() { + return mStart; + } + + /** + * Sets the current value of this NumberPicker, and sets mPrevious to the + * previous value. If current is greater than mEnd less than mStart, the + * value of mCurrent is wrapped around. Subclasses can override this to + * change the wrapping behavior * * @param current the new value of the NumberPicker */ - protected void changeCurrent(int current) { + private void changeCurrent(int current) { + if (mCurrent == current) { + return; + } // Wrap around the values if we go past the start or end - if (current > mEnd) { - current = mStart; - } else if (current < mStart) { - current = mEnd; + if (mWrapSelector) { + current = getWrappedSelectorIndex(current); } - mPrevious = mCurrent; - mCurrent = current; - notifyChange(); - updateView(); + int previous = mCurrent; + setCurrent(current); + notifyChange(previous, current); } /** - * Notifies the listener, if registered, of a change of the value of this - * NumberPicker. + * Sets the <code>alpha</code> of the {@link Paint} for drawing the selector + * wheel. */ - private void notifyChange() { - if (mListener != null) { - mListener.onChanged(this, mPrevious, mCurrent); + @SuppressWarnings("unused") + // Called by ShowInputControlsAnimator via reflection + private void setSelectorPaintAlpha(int alpha) { + mSelectorPaint.setAlpha(alpha); + if (mDrawSelectorWheel) { + invalidate(); } } /** - * Updates the view of this NumberPicker. If displayValues were specified - * in {@link #setRange}, the string corresponding to the index specified by - * the current value will be returned. Otherwise, the formatter specified - * in {@link setFormatter} will be used to format the number. + * @return If the <code>event</code> is in the input text. */ - private void updateView() { - /* 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. - */ - if (mDisplayedValues == null) { - mText.setText(formatNumber(mCurrent)); + private boolean isEventInInputText(MotionEvent event) { + mInputText.getHitRect(mTempRect); + return mTempRect.contains((int) event.getX(), (int) event.getY()); + } + + /** + * Sets if to <code>drawSelectionWheel</code>. + */ + private void setDrawSelectorWheel(boolean drawSelectorWheel) { + mDrawSelectorWheel = drawSelectorWheel; + // do not fade if the selector wheel not shown + setVerticalFadingEdgeEnabled(drawSelectorWheel); + } + + /** + * Callback invoked upon completion of a given <code>scroller</code>. + */ + private void onScrollerFinished(Scroller scroller) { + if (scroller == mFlingScroller) { + postAdjustScrollerCommand(0); } else { - mText.setText(mDisplayedValues[mCurrent - mStart]); + showInputControls(); + updateInputTextView(); } - mText.setSelection(mText.getText().length()); } - private void validateCurrentView(CharSequence str) { - int val = getSelectedPos(str.toString()); - if ((val >= mStart) && (val <= mEnd)) { - if (mCurrent != val) { - mPrevious = mCurrent; - mCurrent = val; - notifyChange(); + /** + * Flings the selector with the given <code>velocityY</code>. + */ + private void fling(int velocityY) { + mPreviousScrollerY = 0; + Scroller flingScroller = mFlingScroller; + + if (mWrapSelector) { + if (velocityY > 0) { + flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); + } else { + flingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE); + } + } else { + if (velocityY > 0) { + int maxY = mTextSize * (mCurrent - mStart); + flingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, maxY); + } else { + int startY = mTextSize * (mEnd - mCurrent); + int maxY = startY; + flingScroller.fling(0, startY, 0, velocityY, 0, 0, 0, maxY); } } - updateView(); + + postAdjustScrollerCommand(flingScroller.getDuration()); + invalidate(); } - private void validateInput(View v) { - String str = String.valueOf(((TextView) v).getText()); - if ("".equals(str)) { + /** + * Hides the input controls which is the up/down arrows and the text field. + */ + private void hideInputControls() { + mShowInputControlsAnimator.cancel(); + mIncrementButton.setVisibility(INVISIBLE); + mDecrementButton.setVisibility(INVISIBLE); + mInputText.setVisibility(INVISIBLE); + } - // Restore to the old value as we don't allow empty values - updateView(); - } else { + /** + * Show the input controls by making them visible and animating the alpha + * property up/down arrows. + */ + private void showInputControls() { + updateIncrementAndDecrementButtonsVisibilityState(); + mInputText.setVisibility(VISIBLE); + mShowInputControlsAnimator.start(); + } - // Check the new value and ensure it's in range - validateCurrentView(str); + /** + * Updates the visibility state of the increment and decrement buttons. + */ + private void updateIncrementAndDecrementButtonsVisibilityState() { + if (mWrapSelector || mCurrent < mEnd) { + mIncrementButton.setVisibility(VISIBLE); + } else { + mIncrementButton.setVisibility(INVISIBLE); + } + if (mWrapSelector || mCurrent > mStart) { + mDecrementButton.setVisibility(VISIBLE); + } else { + mDecrementButton.setVisibility(INVISIBLE); } } /** - * @hide + * @return The selector indices array with proper values with the current as + * the middle one. */ - public void cancelIncrement() { - mIncrement = false; + private int[] getSelectorIndices() { + int current = getCurrent(); + if (mSelectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] != current) { + for (int i = 0; i < mSelectorIndices.length; i++) { + int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX); + if (mWrapSelector) { + selectorIndex = getWrappedSelectorIndex(selectorIndex); + } + mSelectorIndices[i] = selectorIndex; + ensureCachedScrollSelectorValue(mSelectorIndices[i]); + } + } + return mSelectorIndices; } /** - * @hide + * @return The wrapped index <code>selectorIndex</code> value. + * <p> + * Note: The absolute value of the argument is never larger than + * mEnd - mStart. + * </p> */ - public void cancelDecrement() { - mDecrement = false; + private int getWrappedSelectorIndex(int selectorIndex) { + if (selectorIndex > mEnd) { + return (Math.abs(selectorIndex) - mEnd); + } else if (selectorIndex < mStart) { + return (mEnd - Math.abs(selectorIndex)); + } + return selectorIndex; } - private static final char[] DIGIT_CHARACTERS = new char[] { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' - }; + /** + * Increments the <code>selectorIndices</code> whose string representations + * will be displayed in the selector. + */ + private void incrementScrollSelectorIndices(int[] selectorIndices) { + for (int i = 0; i < selectorIndices.length - 1; i++) { + selectorIndices[i] = selectorIndices[i + 1]; + } + int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1; + if (mWrapSelector && nextScrollSelectorIndex > mEnd) { + nextScrollSelectorIndex = mStart; + } + selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex; + ensureCachedScrollSelectorValue(nextScrollSelectorIndex); + } - private NumberPickerButton mIncrementButton; - private NumberPickerButton mDecrementButton; + /** + * Decrements the <code>selectorIndices</code> whose string representations + * will be displayed in the selector. + */ + private void decrementSelectorIndices(int[] selectorIndices) { + for (int i = selectorIndices.length - 1; i > 0; i--) { + selectorIndices[i] = selectorIndices[i - 1]; + } + int nextScrollSelectorIndex = selectorIndices[1] - 1; + if (mWrapSelector && nextScrollSelectorIndex < mStart) { + nextScrollSelectorIndex = mEnd; + } + selectorIndices[0] = nextScrollSelectorIndex; + ensureCachedScrollSelectorValue(nextScrollSelectorIndex); + } - private class NumberPickerInputFilter implements InputFilter { - public CharSequence filter(CharSequence source, int start, int end, - Spanned dest, int dstart, int dend) { - if (mDisplayedValues == null) { - return mNumberInputFilter.filter(source, start, end, dest, dstart, dend); - } - CharSequence filtered = String.valueOf(source.subSequence(start, end)); - String result = String.valueOf(dest.subSequence(0, dstart)) - + filtered - + dest.subSequence(dend, dest.length()); - String str = String.valueOf(result).toLowerCase(); - for (String val : mDisplayedValues) { - val = val.toLowerCase(); - if (val.startsWith(str)) { - return filtered; - } + /** + * 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 < mStart || selectorIndex > mEnd) { + scrollSelectorValue = ""; + } else { + if (mDisplayedValues != null) { + scrollSelectorValue = mDisplayedValues[selectorIndex]; + } else { + scrollSelectorValue = formatNumber(selectorIndex); } - return ""; } + cache.put(selectorIndex, scrollSelectorValue); } - private class NumberRangeKeyListener extends NumberKeyListener { + private String formatNumber(int value) { + return (mFormatter != null) ? mFormatter.toString(value) : String.valueOf(value); + } - // XXX This doesn't allow for range limits when controlled by a - // soft input method! - public int getInputType() { - return InputType.TYPE_CLASS_NUMBER; + private void validateInputTextView(View v) { + String str = String.valueOf(((TextView) v).getText()); + if (TextUtils.isEmpty(str)) { + // Restore to the old value as we don't allow empty values + updateInputTextView(); + } else { + // Check the new value and ensure it's in range + int current = getSelectedPos(str.toString()); + changeCurrent(current); } + } - @Override - protected char[] getAcceptedChars() { - return DIGIT_CHARACTERS; + /** + * Updates the view of this NumberPicker. If displayValues were specified in + * {@link #setRange}, 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. + */ + if (mDisplayedValues == null) { + mInputText.setText(formatNumber(mCurrent)); + } else { + mInputText.setText(mDisplayedValues[mCurrent - mStart]); } + mInputText.setSelection(mInputText.getText().length()); + } - @Override - public CharSequence filter(CharSequence source, int start, int end, - Spanned dest, int dstart, int dend) { - - 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()); + /** + * Notifies the listener, if registered, of a change of the value of this + * NumberPicker. + */ + private void notifyChange(int previous, int current) { + if (mListener != null) { + mListener.onChanged(this, previous, mCurrent); + } + } - if ("".equals(result)) { - return result; - } - int val = getSelectedPos(result); + /** + * Posts a command for updating the current value every <code>updateMillis + * </code>. + */ + private void postUpdateValueFromLongPress(int updateMillis) { + mInputText.clearFocus(); + removeAllCallbacks(); + if (mUpdateFromLongPressCommand == null) { + mUpdateFromLongPressCommand = new UpdateValueFromLongPressCommand(); + } + mUpdateFromLongPressCommand.setUpdateStep(updateMillis); + post(mUpdateFromLongPressCommand); + } - /* 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. - */ - if (val > mEnd) { - return ""; - } else { - return filtered; - } + /** + * Removes all pending callback from the message queue. + */ + private void removeAllCallbacks() { + if (mUpdateFromLongPressCommand != null) { + removeCallbacks(mUpdateFromLongPressCommand); + } + if (mAdjustScrollerCommand != null) { + removeCallbacks(mAdjustScrollerCommand); + } + if (mSetSelectionCommand != null) { + removeCallbacks(mSetSelectionCommand); } } - private int getSelectedPos(String str) { + /** + * @return The selected index given its displayed <code>value</code>. + */ + private int getSelectedPos(String value) { if (mDisplayedValues == null) { try { - return Integer.parseInt(str); + return Integer.parseInt(value); } catch (NumberFormatException e) { - /* Ignore as if it's not a number we don't care */ + // 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 */ - str = str.toLowerCase(); - if (mDisplayedValues[i].toLowerCase().startsWith(str)) { + // Don't force the user to type in jan when ja will do + value = value.toLowerCase(); + if (mDisplayedValues[i].toLowerCase().startsWith(value)) { return mStart + i; } } - /* The user might have typed in a number into the month field i.e. + /* + * 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(str); + return Integer.parseInt(value); } catch (NumberFormatException e) { - /* Ignore as if it's not a number we don't care */ + // Ignore as if it's not a number we don't care } } return mStart; } /** - * Returns the current value of the NumberPicker - * @return the current value. + * Posts an {@link SetSelectionCommand} from the given <code>selectionStart + * </code> to + * <code>selectionEnd</code>. */ - public int getCurrent() { - return mCurrent; + private void postSetSelectionCommand(int selectionStart, int selectionEnd) { + if (mSetSelectionCommand == null) { + mSetSelectionCommand = new SetSelectionCommand(); + } else { + removeCallbacks(mSetSelectionCommand); + } + mSetSelectionCommand.mSelectionStart = selectionStart; + mSetSelectionCommand.mSelectionEnd = selectionEnd; + post(mSetSelectionCommand); } /** - * Returns the upper value of the range of the NumberPicker - * @return the uppper number of the range. + * Posts an {@link AdjustScrollerCommand} within the given <code> + * delayMillis</code> + * . */ - protected int getEndRange() { - return mEnd; + private void postAdjustScrollerCommand(int delayMillis) { + if (mAdjustScrollerCommand == null) { + mAdjustScrollerCommand = new AdjustScrollerCommand(); + } else { + removeCallbacks(mAdjustScrollerCommand); + } + postDelayed(mAdjustScrollerCommand, delayMillis); } /** - * Returns the lower value of the range of the NumberPicker - * @return the lower number of the range. + * Filter for accepting only valid indices or prefixes of the string + * representation of valid indices. */ - protected int getBeginRange() { - return mStart; + 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) { + 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. + */ + if (val > mEnd) { + return ""; + } else { + return filtered; + } + } else { + CharSequence filtered = String.valueOf(source.subSequence(start, end)); + if (TextUtils.isEmpty(filtered)) { + return ""; + } + String result = String.valueOf(dest.subSequence(0, dstart)) + filtered + + dest.subSequence(dend, dest.length()); + String str = String.valueOf(result).toLowerCase(); + for (String val : mDisplayedValues) { + String valLowerCase = val.toLowerCase(); + if (valLowerCase.startsWith(str)) { + postSetSelectionCommand(result.length(), val.length()); + return val.subSequence(dstart, val.length()); + } + } + return ""; + } + } + } + + /** + * Command for setting the input text selection. + */ + class SetSelectionCommand implements Runnable { + private int mSelectionStart; + + private int mSelectionEnd; + + public void run() { + mInputText.setSelection(mSelectionStart, mSelectionEnd); + } + } + + /** + * Command for adjusting the scroller to show in its center the closest of + * the displayed items. + */ + class AdjustScrollerCommand implements Runnable { + public void run() { + mPreviousScrollerY = 0; + int deltaY = mInitialScrollOffset - mCurrentScrollOffset; + float delayCoef = (float) Math.abs(deltaY) / (float) mTextSize; + int duration = (int) (delayCoef * SELECTOR_ADJUSTMENT_DURATION_MILLIS); + mAdjustScroller.startScroll(0, 0, 0, deltaY, duration); + invalidate(); + } + } + + /** + * Command for updating the current value from a long press. + */ + class UpdateValueFromLongPressCommand implements Runnable { + private int mUpdateStep = 0; + + private void setUpdateStep(int updateStep) { + mUpdateStep = updateStep; + } + + public void run() { + changeCurrent(mCurrent + mUpdateStep); + postDelayed(this, mLongPressUpdateSpeed); + } } } diff --git a/core/java/android/widget/NumberPickerButton.java b/core/java/android/widget/NumberPickerButton.java deleted file mode 100644 index 292b668..0000000 --- a/core/java/android/widget/NumberPickerButton.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.widget.ImageButton; -import android.widget.NumberPicker; - -import com.android.internal.R; - -/** - * This class exists purely to cancel long click events, that got - * started in NumberPicker - */ -class NumberPickerButton extends ImageButton { - - private NumberPicker mNumberPicker; - - public NumberPickerButton(Context context, AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); - } - - public NumberPickerButton(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public NumberPickerButton(Context context) { - super(context); - } - - public void setNumberPicker(NumberPicker picker) { - mNumberPicker = picker; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - cancelLongpressIfRequired(event); - return super.onTouchEvent(event); - } - - @Override - public boolean onTrackballEvent(MotionEvent event) { - cancelLongpressIfRequired(event); - return super.onTrackballEvent(event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER) - || (keyCode == KeyEvent.KEYCODE_ENTER)) { - cancelLongpress(); - } - return super.onKeyUp(keyCode, event); - } - - private void cancelLongpressIfRequired(MotionEvent event) { - if ((event.getAction() == MotionEvent.ACTION_CANCEL) - || (event.getAction() == MotionEvent.ACTION_UP)) { - cancelLongpress(); - } - } - - private void cancelLongpress() { - if (R.id.increment == getId()) { - mNumberPicker.cancelIncrement(); - } else if (R.id.decrement == getId()) { - mNumberPicker.cancelDecrement(); - } - } - - public void onWindowFocusChanged(boolean hasWindowFocus) { - super.onWindowFocusChanged(hasWindowFocus); - if (!hasWindowFocus) { - cancelLongpress(); - } - } - -} diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index e61fac3..6cf1387 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -16,6 +16,8 @@ package android.widget; +import com.android.internal.R; + import android.annotation.Widget; import android.content.Context; import android.os.Parcel; @@ -23,9 +25,7 @@ import android.os.Parcelable; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; -import android.widget.NumberPicker; - -import com.android.internal.R; +import android.widget.NumberPicker.OnChangedListener; import java.text.DateFormatSymbols; import java.util.Calendar; @@ -51,7 +51,7 @@ import java.util.Calendar; */ @Widget public class TimePicker extends FrameLayout { - + /** * A no-op callback used in the constructor to avoid null checks * later in the code. @@ -70,10 +70,11 @@ public class TimePicker extends FrameLayout { // ui components private final NumberPicker mHourPicker; private final NumberPicker mMinutePicker; - private final Button mAmPmButton; - private final String mAmText; - private final String mPmText; - + private final NumberPicker mAmPmPicker; + private final TextView mDivider; + + private final String[] mAmPmStrings; + // callbacks private OnTimeChangedListener mOnTimeChangedListener; @@ -127,6 +128,10 @@ public class TimePicker extends FrameLayout { } }); + // divider + mDivider = (TextView) findViewById(R.id.divider); + mDivider.setText(R.string.time_picker_separator); + // digits of minute mMinutePicker = (NumberPicker) findViewById(R.id.minute); mMinutePicker.setRange(0, 59); @@ -140,50 +145,41 @@ public class TimePicker extends FrameLayout { }); // am/pm - mAmPmButton = (Button) findViewById(R.id.amPm); - - // now that the hour/minute picker objects have been initialized, set - // the hour range properly based on the 12/24 hour display mode. - configurePickerRanges(); - - // initialize to current time - Calendar cal = Calendar.getInstance(); - setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); - - // by default we're not in 24 hour mode - setCurrentHour(cal.get(Calendar.HOUR_OF_DAY)); - setCurrentMinute(cal.get(Calendar.MINUTE)); - - mIsAm = (mCurrentHour < 12); - - /* Get the localized am/pm strings and use them in the spinner */ - DateFormatSymbols dfs = new DateFormatSymbols(); - String[] dfsAmPm = dfs.getAmPmStrings(); - mAmText = dfsAmPm[Calendar.AM]; - mPmText = dfsAmPm[Calendar.PM]; - mAmPmButton.setText(mIsAm ? mAmText : mPmText); - mAmPmButton.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - requestFocus(); + mAmPmPicker = (NumberPicker) findViewById(R.id.amPm); + mAmPmPicker.setOnChangeListener(new OnChangedListener() { + public void onChanged(NumberPicker picker, int oldVal, int newVal) { + picker.requestFocus(); if (mIsAm) { - // Currently AM switching to PM if (mCurrentHour < 12) { mCurrentHour += 12; - } + } } else { - // Currently PM switching to AM if (mCurrentHour >= 12) { mCurrentHour -= 12; } } mIsAm = !mIsAm; - mAmPmButton.setText(mIsAm ? mAmText : mPmText); onTimeChanged(); } }); - + + /* Get the localized am/pm strings and use them in the spinner */ + mAmPmStrings = new DateFormatSymbols().getAmPmStrings(); + + // now that the hour/minute picker objects have been initialized, set + // the hour range properly based on the 12/24 hour display mode. + configurePickerRanges(); + + // initialize to current time + Calendar cal = Calendar.getInstance(); + setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); + + // by default we're not in 24 hour mode + setCurrentHour(cal.get(Calendar.HOUR_OF_DAY)); + setCurrentMinute(cal.get(Calendar.MINUTE)); + if (!isEnabled()) { setEnabled(false); } @@ -194,7 +190,7 @@ public class TimePicker extends FrameLayout { super.setEnabled(enabled); mMinutePicker.setEnabled(enabled); mHourPicker.setEnabled(enabled); - mAmPmButton.setEnabled(enabled); + mAmPmPicker.setEnabled(enabled); } /** @@ -327,12 +323,15 @@ public class TimePicker extends FrameLayout { int currentHour = mCurrentHour; if (!mIs24HourView) { // convert [0,23] ordinal to wall clock display - if (currentHour > 12) currentHour -= 12; - else if (currentHour == 0) currentHour = 12; + if (currentHour > 12) { + currentHour -= 12; + } else if (currentHour == 0) { + currentHour = 12; + } } mHourPicker.setCurrent(currentHour); mIsAm = mCurrentHour < 12; - mAmPmButton.setText(mIsAm ? mAmText : mPmText); + mAmPmPicker.setCurrent(mIsAm ? Calendar.AM : Calendar.PM); onTimeChanged(); } @@ -340,16 +339,19 @@ public class TimePicker extends FrameLayout { if (mIs24HourView) { mHourPicker.setRange(0, 23); mHourPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER); - mAmPmButton.setVisibility(View.GONE); + mAmPmPicker.setVisibility(View.GONE); } else { mHourPicker.setRange(1, 12); mHourPicker.setFormatter(null); - mAmPmButton.setVisibility(View.VISIBLE); + mAmPmPicker.setVisibility(View.VISIBLE); + mAmPmPicker.setRange(0, 1, mAmPmStrings); } } private void onTimeChanged() { - mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute()); + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute()); + } } /** @@ -357,6 +359,6 @@ public class TimePicker extends FrameLayout { */ private void updateMinuteDisplay() { mMinutePicker.setCurrent(mCurrentMinute); - mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute()); + onTimeChanged(); } } diff --git a/core/res/res/drawable/timepicker_down_btn.xml b/core/res/res/drawable/timepicker_down_btn.xml index 61a252a..d4908cb 100644 --- a/core/res/res/drawable/timepicker_down_btn.xml +++ b/core/res/res/drawable/timepicker_down_btn.xml @@ -16,15 +16,28 @@ <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_pressed="false" android:state_enabled="true" - android:state_focused="false" android:drawable="@drawable/timepicker_down_normal" /> - <item android:state_pressed="true" android:state_enabled="true" + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="false" + android:drawable="@drawable/timepicker_down_normal" /> + + <item android:state_pressed="true" + android:state_enabled="true" android:drawable="@drawable/timepicker_down_pressed" /> - <item android:state_pressed="false" android:state_enabled="true" - android:state_focused="true" android:drawable="@drawable/timepicker_down_selected" /> - <item android:state_pressed="false" android:state_enabled="false" - android:state_focused="false" android:drawable="@drawable/timepicker_down_disabled" /> - <item android:state_pressed="false" android:state_enabled="false" - android:state_focused="true" android:drawable="@drawable/timepicker_down_disabled_focused" /> + + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="true" + android:drawable="@drawable/timepicker_down_selected" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="false" + android:drawable="@drawable/timepicker_down_disabled" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="true" + android:drawable="@drawable/timepicker_down_disabled_focused" /> </selector> diff --git a/core/res/res/drawable/timepicker_down_btn_holo_dark.xml b/core/res/res/drawable/timepicker_down_btn_holo_dark.xml new file mode 100644 index 0000000..b4b824d --- /dev/null +++ b/core/res/res/drawable/timepicker_down_btn_holo_dark.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="false" + android:drawable="@drawable/timepicker_down_normal_holo_dark" /> + + <item android:state_pressed="true" + android:state_enabled="true" + android:drawable="@drawable/timepicker_down_pressed_holo_dark" /> + + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="true" + android:drawable="@drawable/timepicker_down_focused_holo_dark" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="false" + android:drawable="@drawable/timepicker_down_disabled_holo_dark" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="true" + android:drawable="@drawable/timepicker_down_disabled_focused_holo_dark" /> + +</selector> diff --git a/core/res/res/drawable/timepicker_down_btn_holo_light.xml b/core/res/res/drawable/timepicker_down_btn_holo_light.xml new file mode 100644 index 0000000..7ae5e53 --- /dev/null +++ b/core/res/res/drawable/timepicker_down_btn_holo_light.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="false" + android:drawable="@drawable/timepicker_down_normal_holo_light" /> + + <item android:state_pressed="true" + android:state_enabled="true" + android:drawable="@drawable/timepicker_down_pressed_holo_light" /> + + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="true" + android:drawable="@drawable/timepicker_down_focused_holo_light" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="false" + android:drawable="@drawable/timepicker_down_disabled_holo_light" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="true" + android:drawable="@drawable/timepicker_down_disabled_focused_holo_light" /> + +</selector> diff --git a/core/res/res/drawable/timepicker_input.xml b/core/res/res/drawable/timepicker_input.xml index b811d4e..1768673 100644 --- a/core/res/res/drawable/timepicker_input.xml +++ b/core/res/res/drawable/timepicker_input.xml @@ -16,15 +16,28 @@ <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_pressed="false" android:state_enabled="true" - android:state_focused="false" android:drawable="@drawable/timepicker_input_normal" /> - <item android:state_pressed="true" android:state_enabled="true" + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="false" + android:drawable="@drawable/timepicker_input_normal" /> + + <item android:state_pressed="true" + android:state_enabled="true" android:drawable="@drawable/timepicker_input_pressed" /> - <item android:state_pressed="false" android:state_enabled="true" - android:state_focused="true" android:drawable="@drawable/timepicker_input_selected" /> - <item android:state_pressed="false" android:state_enabled="false" - android:state_focused="false" android:drawable="@drawable/timepicker_input_disabled" /> - <item android:state_pressed="false" android:state_enabled="false" - android:state_focused="true" android:drawable="@drawable/timepicker_input_normal" /> + + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="true" + android:drawable="@drawable/timepicker_input_selected" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="false" + android:drawable="@drawable/timepicker_input_disabled" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="true" + android:drawable="@drawable/timepicker_input_normal" /> </selector> diff --git a/core/res/res/drawable/timepicker_up_btn.xml b/core/res/res/drawable/timepicker_up_btn.xml index 5428aee..fbaed08 100644 --- a/core/res/res/drawable/timepicker_up_btn.xml +++ b/core/res/res/drawable/timepicker_up_btn.xml @@ -16,15 +16,28 @@ <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_pressed="false" android:state_enabled="true" - android:state_focused="false" android:drawable="@drawable/timepicker_up_normal" /> - <item android:state_pressed="true" android:state_enabled="true" + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="false" + android:drawable="@drawable/timepicker_up_normal" /> + + <item android:state_pressed="true" + android:state_enabled="true" android:drawable="@drawable/timepicker_up_pressed" /> - <item android:state_pressed="false" android:state_enabled="true" - android:state_focused="true" android:drawable="@drawable/timepicker_up_selected" /> - <item android:state_pressed="false" android:state_enabled="false" - android:state_focused="false" android:drawable="@drawable/timepicker_up_disabled" /> - <item android:state_pressed="false" android:state_enabled="false" - android:state_focused="true" android:drawable="@drawable/timepicker_up_disabled_focused" /> + + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="true" + android:drawable="@drawable/timepicker_up_selected" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="false" + android:drawable="@drawable/timepicker_up_disabled" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="true" + android:drawable="@drawable/timepicker_up_disabled_focused" /> </selector> diff --git a/core/res/res/drawable/timepicker_up_btn_holo_dark.xml b/core/res/res/drawable/timepicker_up_btn_holo_dark.xml new file mode 100644 index 0000000..af5f6eb --- /dev/null +++ b/core/res/res/drawable/timepicker_up_btn_holo_dark.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="false" + android:drawable="@drawable/timepicker_up_normal_holo_dark" /> + + <item android:state_pressed="true" + android:state_enabled="true" + android:drawable="@drawable/timepicker_up_pressed_holo_dark" /> + + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="true" + android:drawable="@drawable/timepicker_up_focused_holo_dark" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="false" + android:drawable="@drawable/timepicker_up_disabled_holo_dark" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="true" + android:drawable="@drawable/timepicker_up_disabled_focused_holo_dark" /> + +</selector> diff --git a/core/res/res/drawable/timepicker_up_btn_holo_light.xml b/core/res/res/drawable/timepicker_up_btn_holo_light.xml new file mode 100644 index 0000000..6025d3a --- /dev/null +++ b/core/res/res/drawable/timepicker_up_btn_holo_light.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="false" + android:drawable="@drawable/timepicker_up_normal_holo_light" /> + + <item android:state_pressed="true" + android:state_enabled="true" + android:drawable="@drawable/timepicker_up_pressed_holo_light" /> + + <item android:state_pressed="false" + android:state_enabled="true" + android:state_focused="true" + android:drawable="@drawable/timepicker_up_focused_holo_light" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="false" + android:drawable="@drawable/timepicker_up_focused_holo_light" /> + + <item android:state_pressed="false" + android:state_enabled="false" + android:state_focused="true" + android:drawable="@drawable/timepicker_up_disabled_focused_holo_light" /> + +</selector> diff --git a/core/res/res/layout/number_picker.xml b/core/res/res/layout/number_picker.xml index 9241708..44c99cf 100644 --- a/core/res/res/layout/number_picker.xml +++ b/core/res/res/layout/number_picker.xml @@ -19,24 +19,19 @@ <merge xmlns:android="http://schemas.android.com/apk/res/android"> - <NumberPickerButton android:id="@+id/increment" - android:layout_width="match_parent" + <ImageButton android:id="@+id/increment" + android:layout_width="fill_parent" android:layout_height="wrap_content" - android:background="@drawable/timepicker_up_btn" /> + style="?android:attr/numberPickerUpButtonStyle" /> <EditText android:id="@+id/timepicker_input" - android:layout_width="match_parent" + android:layout_width="fill_parent" android:layout_height="wrap_content" - android:gravity="center" - android:singleLine="true" - style="?android:attr/textAppearanceLargeInverse" - android:textColor="@android:color/primary_text_light" - android:textSize="30sp" - android:background="@drawable/timepicker_input" /> + style="?android:attr/numberPickerInputTextStyle" /> - <NumberPickerButton android:id="@+id/decrement" - android:layout_width="match_parent" + <ImageButton android:id="@+id/decrement" + android:layout_width="fill_parent" android:layout_height="wrap_content" - android:background="@drawable/timepicker_down_btn" /> + style="?android:attr/numberPickerDownButtonStyle" /> </merge> diff --git a/core/res/res/layout/time_picker.xml b/core/res/res/layout/time_picker.xml index 6124ea8..fa28842 100644 --- a/core/res/res/layout/time_picker.xml +++ b/core/res/res/layout/time_picker.xml @@ -28,32 +28,46 @@ <!-- hour --> <NumberPicker android:id="@+id/hour" - android:layout_width="70dip" + android:layout_width="48dip" android:layout_height="wrap_content" + android:layout_marginRight="20dip" + android:layout_marginTop="35dip" + android:layout_marginBottom="35dip" android:focusable="true" android:focusableInTouchMode="true" /> - + + <!-- divider --> + <TextView + android:id="@+id/divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + /> + <!-- minute --> <NumberPicker android:id="@+id/minute" - android:layout_width="70dip" + android:layout_width="48dip" android:layout_height="wrap_content" - android:layout_marginLeft="5dip" + android:layout_marginLeft="20dip" + android:layout_marginRight="22dip" + android:layout_marginTop="35dip" + android:layout_marginBottom="35dip" android:focusable="true" android:focusableInTouchMode="true" /> - + <!-- AM / PM --> - <Button + <NumberPicker android:id="@+id/amPm" - android:layout_width="wrap_content" + android:layout_width="48dip" android:layout_height="wrap_content" - android:layout_marginTop="43dip" - android:layout_marginLeft="5dip" - android:paddingLeft="20dip" - android:paddingRight="20dip" - style="?android:attr/textAppearanceLargeInverse" - android:textColor="@android:color/primary_text_light_nodisable" + android:layout_marginLeft="22dip" + android:layout_marginTop="35dip" + android:layout_marginBottom="35dip" + android:focusable="true" + android:focusableInTouchMode="true" /> + </LinearLayout> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index a8099e3..5d4fd4e 100755 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -503,6 +503,15 @@ <attr name="listPopupWindowStyle" format="reference" /> <!-- Default PopupMenu style. --> <attr name="popupMenuStyle" format="reference" /> + <!-- NumberPicker up button style --> + <attr name="numberPickerUpButtonStyle" format="reference" /> + <!-- NumberPicker down button style --> + <attr name="numberPickerDownButtonStyle" format="reference" /> + <!-- NumberPicker input text style --> + <attr name="numberPickerInputTextStyle" format="reference" /> + + <!-- NumberPicker the fading edge length of the selector wheel --> + <attr name="numberPickerStyle" format="reference" /> <!-- =================== --> <!-- Action bar styles --> @@ -2856,6 +2865,12 @@ <attr name="minorWeightMax" format="float" /> </declare-styleable> + <!-- @hide --> + <declare-styleable name="NumberPicker"> + <attr name="orientation" /> + <attr name="solidColor" format="color|reference" /> + </declare-styleable> + <!-- ========================= --> <!-- Drawable class attributes --> <!-- ========================= --> diff --git a/core/res/res/values/donottranslate.xml b/core/res/res/values/donottranslate.xml index d6d5dbb..bdcab39 100644 --- a/core/res/res/values/donottranslate.xml +++ b/core/res/res/values/donottranslate.xml @@ -24,4 +24,6 @@ <bool name="lockscreen_isPortrait">true</bool> <!-- @hide DO NOT TRANSLATE. Control aspect ratio of lock pattern --> <string name="lock_pattern_view_aspect">square</string> + <!-- @hide DO NOT TRANSLATE. Separator between the hour and minute elements in a TimePicker widget --> + <string name="time_picker_separator">:</string> </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 0c3361d..eafa9b6 100755 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -2291,6 +2291,10 @@ <!-- See SMS_DIALOG. This is a button choice to disallow sending the SMSes.. --> <string name="sms_control_no">Cancel</string> + <!-- Date/Time picker dialogs strings --> + + <!-- The title of the time picker dialog. [CHAR LIMIT=NONE] --> + <string name="time_picker_dialog_title">Set time</string> <!-- Name of the button in the date/time picker to accept the date/time change --> <string name="date_time_set">Set</string> diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index 2754e73..60150a1 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -460,6 +460,28 @@ <item name="android:background">@android:drawable/btn_default</item> </style> + <style name="Widget.NumberPicker"> + <item name="android:orientation">vertical</item> + <item name="android:fadingEdge">vertical</item> + <item name="android:fadingEdgeLength">50dip</item> + <item name="android:solidColor">?android:attr/colorBackground</item> + </style> + + <style name="Widget.ImageButton.NumberPickerUpButton"> + <item name="android:background">@android:drawable/timepicker_up_btn</item> + </style> + + <style name="Widget.ImageButton.NumberPickerDownButton"> + <item name="android:background">@android:drawable/timepicker_down_btn</item> + </style> + + <style name="Widget.EditText.NumberPickerInputText"> + <item name="android:textAppearance">@style/TextAppearance.Large.Inverse.NumberPickerInputText</item> + <item name="android:gravity">center</item> + <item name="android:singleLine">true</item> + <item name="android:background">@drawable/timepicker_input</item> + </style> + <style name="Widget.AutoCompleteTextView" parent="Widget.EditText"> <item name="android:completionHintView">@android:layout/simple_dropdown_hint</item> <item name="android:completionThreshold">2</item> @@ -761,7 +783,7 @@ <style name="TextAppearance.Widget.TextView.SpinnerItem"> <item name="android:textColor">@android:color/primary_text_light_disable_only</item> </style> - + <!-- @hide --> <style name="TextAppearance.SlidingTabNormal" parent="@android:attr/textAppearanceMedium"> @@ -804,6 +826,11 @@ <item name="android:textStyle">bold</item> </style> + <style name="TextAppearance.Large.Inverse.NumberPickerInputText"> + <item name="android:textColor">@android:color/primary_text_light</item> + <item name="android:textSize">30sp</item> + </style> + <!-- Preference Styles --> <style name="Preference"> @@ -1353,6 +1380,27 @@ <style name="Widget.Holo.ImageButton" parent="Widget.ImageButton"> </style> + <style name="Widget.Holo.ImageButton.NumberPickerUpButton"> + <item name="android:background">@null</item> + <item name="android:paddingBottom">26sp</item> + <item name="android:src">@android:drawable/timepicker_up_btn_holo_dark</item> + </style> + + <style name="Widget.Holo.ImageButton.NumberPickerDownButton"> + <item name="android:background">@null</item> + <item name="android:paddingTop">26sp</item> + <item name="android:src">@android:drawable/timepicker_down_btn_holo_dark</item> + </style> + + <style name="Widget.Holo.EditText.NumberPickerInputText"> + <item name="android:paddingTop">13sp</item> + <item name="android:paddingBottom">13sp</item> + <item name="android:gravity">center</item> + <item name="android:singleLine">true</item> + <item name="android:textSize">18sp</item> + <item name="android:background">@null</item> + </style> + <style name="Widget.Holo.ImageWell" parent="Widget.ImageWell"> </style> @@ -1655,6 +1703,17 @@ <style name="Widget.Holo.Light.ImageButton" parent="Widget.ImageButton"> </style> + <style name="Widget.Holo.Light.ImageButton.NumberPickerUpButton" parent="Widget.Holo.Light.ImageButton.NumberPickerUpButton"> + <item name="android:background">@android:drawable/timepicker_up_btn_holo_light</item> + </style> + + <style name="Widget.Holo.Light.ImageButton.NumberPickerDownButton" parent="Widget.Holo.ImageButton.NumberPickerDownButton"> + <item name="android:background">@android:drawable/timepicker_down_btn_holo_light</item> + </style> + + <style name="Widget.Holo.Light.EditText.NumberPickerInputText" parent="Widget.Holo.EditText.NumberPickerInputText"> + </style> + <style name="Widget.Holo.Light.ImageWell" parent="Widget.ImageWell"> </style> diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml index 0eec0df..97df48a 100644 --- a/core/res/res/values/themes.xml +++ b/core/res/res/values/themes.xml @@ -267,8 +267,16 @@ <item name="searchViewGoIcon">@android:drawable/ic_go</item> <item name="searchViewVoiceIcon">@android:drawable/ic_voice_search</item> + <!-- PreferenceFrameLayout attributes --> <item name="preferenceFrameLayoutStyle">@android:style/Widget.PreferenceFrameLayout</item> + + <!-- NumberPicker styles--> + <item name="numberPickerUpButtonStyle">@style/Widget.ImageButton.NumberPickerUpButton</item> + <item name="numberPickerDownButtonStyle">@style/Widget.ImageButton.NumberPickerDownButton</item> + <item name="numberPickerInputTextStyle">@style/Widget.EditText.NumberPickerInputText</item> + <item name="numberPickerStyle">@style/Widget.NumberPicker</item> + </style> <!-- Variant of the default (dark) theme with no title bar --> @@ -881,6 +889,12 @@ <!-- PreferenceFrameLayout attributes --> <item name="preferenceFrameLayoutStyle">@android:style/Widget.Holo.PreferenceFrameLayout</item> + + <!-- NumberPicker styles--> + <item name="numberPickerUpButtonStyle">@style/Widget.Holo.ImageButton.NumberPickerUpButton</item> + <item name="numberPickerDownButtonStyle">@style/Widget.Holo.ImageButton.NumberPickerDownButton</item> + <item name="numberPickerInputTextStyle">@style/Widget.Holo.EditText.NumberPickerInputText</item> + </style> <!-- New Honeycomb holographic theme. Light version. The widgets in the @@ -1117,6 +1131,12 @@ <!-- SearchView attributes --> <item name="searchDropdownBackground">@android:drawable/search_dropdown_light</item> + + <!-- NumberPicker attributes and styles--> + <item name="numberPickerUpButtonStyle">@style/Widget.Holo.Light.ImageButton.NumberPickerUpButton</item> + <item name="numberPickerDownButtonStyle">@style/Widget.Holo.Light.ImageButton.NumberPickerDownButton</item> + <item name="numberPickerInputTextStyle">@style/Widget.Holo.Light.EditText.NumberPickerInputText</item> + </style> <!-- Development legacy name; if you're using this, switch. --> |