diff options
Diffstat (limited to 'core/java/android/widget/TimePickerSpinnerDelegate.java')
-rw-r--r-- | core/java/android/widget/TimePickerSpinnerDelegate.java | 1405 |
1 files changed, 379 insertions, 1026 deletions
diff --git a/core/java/android/widget/TimePickerSpinnerDelegate.java b/core/java/android/widget/TimePickerSpinnerDelegate.java index 73e05e8..e162f4a 100644 --- a/core/java/android/widget/TimePickerSpinnerDelegate.java +++ b/core/java/android/widget/TimePickerSpinnerDelegate.java @@ -17,380 +17,365 @@ package android.widget; import android.content.Context; -import android.content.res.ColorStateList; import android.content.res.Configuration; -import android.content.res.Resources; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; -import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.AttributeSet; -import android.util.Log; -import android.view.HapticFeedbackConstants; -import android.view.KeyCharacterMap; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; - +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; import com.android.internal.R; -import java.util.ArrayList; +import java.text.DateFormatSymbols; import java.util.Calendar; import java.util.Locale; -/** - * A view for selecting the time of day, in either 24 hour or AM/PM mode. - */ -class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate implements - RadialTimePickerView.OnValueSelectedListener { - - private static final String TAG = "TimePickerDelegate"; - - // Index used by RadialPickerLayout - private static final int HOUR_INDEX = 0; - private static final int MINUTE_INDEX = 1; - - // NOT a real index for the purpose of what's showing. - private static final int AMPM_INDEX = 2; +import libcore.icu.LocaleData; - // Also NOT a real index, just used for keyboard mode. - private static final int ENABLE_PICKER_INDEX = 3; - - private static final int AM = 0; - private static final int PM = 1; +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; +/** + * A delegate implementing the basic spinner-based TimePicker. + */ +class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate { private static final boolean DEFAULT_ENABLED_STATE = true; - private boolean mIsEnabled = DEFAULT_ENABLED_STATE; - private static final int HOURS_IN_HALF_DAY = 12; - private View mHeaderView; - private TextView mHourView; - private TextView mMinuteView; - private TextView mAmPmTextView; - private RadialTimePickerView mRadialTimePickerView; - private TextView mSeparatorView; + // state + private boolean mIs24HourView; + private boolean mIsAm; - private String mAmText; - private String mPmText; + // ui components + private final NumberPicker mHourSpinner; + private final NumberPicker mMinuteSpinner; + private final NumberPicker mAmPmSpinner; + private final EditText mHourSpinnerInput; + private final EditText mMinuteSpinnerInput; + private final EditText mAmPmSpinnerInput; + private final TextView mDivider; - private boolean mAllowAutoAdvance; - private int mInitialHourOfDay; - private int mInitialMinute; - private boolean mIs24HourView; + // Note that the legacy implementation of the TimePicker is + // using a button for toggling between AM/PM while the new + // version uses a NumberPicker spinner. Therefore the code + // accommodates these two cases to be backwards compatible. + private final Button mAmPmButton; - // For hardware IME input. - private char mPlaceholderText; - private String mDoublePlaceholderText; - private String mDeletedKeyFormat; - private boolean mInKbMode; - private ArrayList<Integer> mTypedTimes = new ArrayList<Integer>(); - private Node mLegalTimesTree; - private int mAmKeyCode; - private int mPmKeyCode; - - // Accessibility strings. - private String mHourPickerDescription; - private String mSelectHours; - private String mMinutePickerDescription; - private String mSelectMinutes; + private final String[] mAmPmStrings; + private boolean mIsEnabled = DEFAULT_ENABLED_STATE; private Calendar mTempCalendar; + private boolean mHourWithTwoDigit; + private char mHourFormat; public TimePickerSpinnerDelegate(TimePicker delegator, Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(delegator, context); // process style attributes - final TypedArray a = mContext.obtainStyledAttributes(attrs, - R.styleable.TimePicker, defStyleAttr, defStyleRes); - final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - final Resources res = mContext.getResources(); - - mHourPickerDescription = res.getString(R.string.hour_picker_description); - mSelectHours = res.getString(R.string.select_hours); - mMinutePickerDescription = res.getString(R.string.minute_picker_description); - mSelectMinutes = res.getString(R.string.select_minutes); - - String[] amPmStrings = TimePickerClockDelegate.getAmPmStrings(context); - mAmText = amPmStrings[0]; - mPmText = amPmStrings[1]; - - final int layoutResourceId = a.getResourceId(R.styleable.TimePicker_internalLayout, - R.layout.time_picker_holo); - final View mainView = inflater.inflate(layoutResourceId, null); - mDelegator.addView(mainView); - - mHourView = (TextView) mainView.findViewById(R.id.hours); - mSeparatorView = (TextView) mainView.findViewById(R.id.separator); - mMinuteView = (TextView) mainView.findViewById(R.id.minutes); - mAmPmTextView = (TextView) mainView.findViewById(R.id.ampm_label); - - // Set up text appearances from style. - final int headerTimeTextAppearance = a.getResourceId( - R.styleable.TimePicker_headerTimeTextAppearance, 0); - if (headerTimeTextAppearance != 0) { - mHourView.setTextAppearance(context, headerTimeTextAppearance); - mSeparatorView.setTextAppearance(context, headerTimeTextAppearance); - mMinuteView.setTextAppearance(context, headerTimeTextAppearance); - } + final TypedArray a = mContext.obtainStyledAttributes( + attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes); + final int layoutResourceId = a.getResourceId( + R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy); + a.recycle(); - final int headerSelectedTextColor = a.getColor( - R.styleable.TimePicker_headerSelectedTextColor, - res.getColor(R.color.timepicker_default_selector_color_material)); - mHourView.setTextColor(ColorStateList.addFirstIfMissing(mHourView.getTextColors(), - R.attr.state_selected, headerSelectedTextColor)); - mMinuteView.setTextColor(ColorStateList.addFirstIfMissing(mMinuteView.getTextColors(), - R.attr.state_selected, headerSelectedTextColor)); - - final int headerAmPmTextAppearance = a.getResourceId( - R.styleable.TimePicker_headerAmPmTextAppearance, 0); - if (headerAmPmTextAppearance != 0) { - mAmPmTextView.setTextAppearance(context, headerAmPmTextAppearance); + final LayoutInflater inflater = LayoutInflater.from(mContext); + inflater.inflate(layoutResourceId, mDelegator, true); + + // hour + mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { + updateInputState(); + if (!is24HourView()) { + if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) || + (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + } + onTimeChanged(); + } + }); + mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input); + mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + + // divider (only for the new widget style) + mDivider = (TextView) mDelegator.findViewById(R.id.divider); + if (mDivider != null) { + setDividerText(); + } + + // minute + mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute); + mMinuteSpinner.setMinValue(0); + mMinuteSpinner.setMaxValue(59); + mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); + mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { + updateInputState(); + int minValue = mMinuteSpinner.getMinValue(); + int maxValue = mMinuteSpinner.getMaxValue(); + if (oldVal == maxValue && newVal == minValue) { + int newHour = mHourSpinner.getValue() + 1; + if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + mHourSpinner.setValue(newHour); + } else if (oldVal == minValue && newVal == maxValue) { + int newHour = mHourSpinner.getValue() - 1; + if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + mHourSpinner.setValue(newHour); + } + onTimeChanged(); + } + }); + mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input); + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + + // Get the localized am/pm strings and use them in the spinner. + mAmPmStrings = getAmPmStrings(context); + + // am/pm + final View amPmView = mDelegator.findViewById(R.id.amPm); + if (amPmView instanceof Button) { + mAmPmSpinner = null; + mAmPmSpinnerInput = null; + mAmPmButton = (Button) amPmView; + mAmPmButton.setOnClickListener(new View.OnClickListener() { + public void onClick(View button) { + button.requestFocus(); + mIsAm = !mIsAm; + updateAmPmControl(); + onTimeChanged(); + } + }); + } else { + mAmPmButton = null; + mAmPmSpinner = (NumberPicker) amPmView; + mAmPmSpinner.setMinValue(0); + mAmPmSpinner.setMaxValue(1); + mAmPmSpinner.setDisplayedValues(mAmPmStrings); + mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + updateInputState(); + picker.requestFocus(); + mIsAm = !mIsAm; + updateAmPmControl(); + onTimeChanged(); + } + }); + mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input); + mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); + } + + if (isAmPmAtStart()) { + // Move the am/pm view to the beginning + ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout); + amPmParent.removeView(amPmView); + amPmParent.addView(amPmView, 0); + // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme + // for example and not for Holo Theme) + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams(); + final int startMargin = lp.getMarginStart(); + final int endMargin = lp.getMarginEnd(); + if (startMargin != endMargin) { + lp.setMarginStart(endMargin); + lp.setMarginEnd(startMargin); + } } - mHeaderView = mainView.findViewById(R.id.time_header); - mHeaderView.setBackground(a.getDrawable(R.styleable.TimePicker_headerBackground)); - - a.recycle(); + getHourFormatData(); - mRadialTimePickerView = (RadialTimePickerView) mainView.findViewById( - R.id.radial_picker); + // update controls to initial state + updateHourControl(); + updateMinuteControl(); + updateAmPmControl(); - setupListeners(); + // set to current time + setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); + setCurrentMinute(mTempCalendar.get(Calendar.MINUTE)); - mAllowAutoAdvance = true; + if (!isEnabled()) { + setEnabled(false); + } - // Set up for keyboard mode. - mDoublePlaceholderText = res.getString(R.string.time_placeholder); - mDeletedKeyFormat = res.getString(R.string.deleted_key); - mPlaceholderText = mDoublePlaceholderText.charAt(0); - mAmKeyCode = mPmKeyCode = -1; - generateLegalTimesTree(); + // set the content descriptions + setContentDescriptions(); - // Initialize with current time - final Calendar calendar = Calendar.getInstance(mCurrentLocale); - final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); - final int currentMinute = calendar.get(Calendar.MINUTE); - initialize(currentHour, currentMinute, false /* 12h */, HOUR_INDEX); - } - - private void initialize(int hourOfDay, int minute, boolean is24HourView, int index) { - mInitialHourOfDay = hourOfDay; - mInitialMinute = minute; - mIs24HourView = is24HourView; - mInKbMode = false; - updateUI(index); + // If not explicitly specified this view is important for accessibility. + if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } - private void setupListeners() { - mHeaderView.setOnKeyListener(mKeyListener); - mHeaderView.setOnFocusChangeListener(mFocusListener); - mHeaderView.setFocusable(true); - - mRadialTimePickerView.setOnValueSelectedListener(this); - - mHourView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - setCurrentItemShowing(HOUR_INDEX, true, true); - tryVibrate(); - } - }); - mMinuteView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - setCurrentItemShowing(MINUTE_INDEX, true, true); - tryVibrate(); + private void getHourFormatData() { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + (mIs24HourView) ? "Hm" : "hm"); + final int lengthPattern = bestDateTimePattern.length(); + mHourWithTwoDigit = false; + char hourFormat = '\0'; + // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save + // the hour format that we found. + for (int i = 0; i < lengthPattern; i++) { + final char c = bestDateTimePattern.charAt(i); + if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { + mHourFormat = c; + if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { + mHourWithTwoDigit = true; + } + break; } - }); + } } - private void updateUI(int index) { - // Update RadialPicker values - updateRadialPicker(index); - // Enable or disable the AM/PM view. - updateHeaderAmPm(); - // Update Hour and Minutes - updateHeaderHour(mInitialHourOfDay, true); - // Update time separator - updateHeaderSeparator(); - // Update Minutes - updateHeaderMinute(mInitialMinute); - // Invalidate everything - mDelegator.invalidate(); - } + private boolean isAmPmAtStart() { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + "hm" /* skeleton */); - private void updateRadialPicker(int index) { - mRadialTimePickerView.initialize(mInitialHourOfDay, mInitialMinute, mIs24HourView); - setCurrentItemShowing(index, false, true); + return bestDateTimePattern.startsWith("a"); } - private int computeMaxWidthOfNumbers(int max) { - TextView tempView = new TextView(mContext); - tempView.setTextAppearance(mContext, R.style.TextAppearance_Material_TimePicker_TimeLabel); - ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT); - tempView.setLayoutParams(lp); - int maxWidth = 0; - for (int minutes = 0; minutes < max; minutes++) { - final String text = String.format("%02d", minutes); - tempView.setText(text); - tempView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); - maxWidth = Math.max(maxWidth, tempView.getMeasuredWidth()); + /** + * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". + * + * See http://unicode.org/cldr/trac/browser/trunk/common/main + * + * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the + * separator as the character which is just after the hour marker in the returned pattern. + */ + private void setDividerText() { + final String skeleton = (mIs24HourView) ? "Hm" : "hm"; + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + skeleton); + final String separatorText; + int hourIndex = bestDateTimePattern.lastIndexOf('H'); + if (hourIndex == -1) { + hourIndex = bestDateTimePattern.lastIndexOf('h'); } - return maxWidth; - } - - private void updateHeaderAmPm() { - if (mIs24HourView) { - mAmPmTextView.setVisibility(View.GONE); + if (hourIndex == -1) { + // Default case + separatorText = ":"; } else { - mAmPmTextView.setVisibility(View.VISIBLE); - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, - "hm"); - - boolean amPmOnLeft = bestDateTimePattern.startsWith("a"); - if (TextUtils.getLayoutDirectionFromLocale(mCurrentLocale) == - View.LAYOUT_DIRECTION_RTL) { - amPmOnLeft = !amPmOnLeft; - } - - RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) - mAmPmTextView.getLayoutParams(); - - if (amPmOnLeft) { - layoutParams.rightMargin = computeMaxWidthOfNumbers(12 /* for hours */); - layoutParams.removeRule(RelativeLayout.RIGHT_OF); - layoutParams.addRule(RelativeLayout.LEFT_OF, R.id.separator); + int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1); + if (minuteIndex == -1) { + separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1)); } else { - layoutParams.leftMargin = computeMaxWidthOfNumbers(60 /* for minutes */); - layoutParams.removeRule(RelativeLayout.LEFT_OF); - layoutParams.addRule(RelativeLayout.RIGHT_OF, R.id.separator); + separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex); } - - updateAmPmDisplay(mInitialHourOfDay < 12 ? AM : PM); - mAmPmTextView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - tryVibrate(); - int amOrPm = mRadialTimePickerView.getAmOrPm(); - if (amOrPm == AM) { - amOrPm = PM; - } else if (amOrPm == PM){ - amOrPm = AM; - } - updateAmPmDisplay(amOrPm); - mRadialTimePickerView.setAmOrPm(amOrPm); - } - }); } + mDivider.setText(separatorText); } - /** - * Set the current hour. - */ @Override public void setCurrentHour(Integer currentHour) { - if (mInitialHourOfDay == currentHour) { + setCurrentHour(currentHour, true); + } + + private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) { + // why was Integer used in the first place? + if (currentHour == null || currentHour == getCurrentHour()) { return; } - mInitialHourOfDay = currentHour; - updateHeaderHour(currentHour, true /* accessibility announce */); - updateHeaderAmPm(); - mRadialTimePickerView.setCurrentHour(currentHour); - mRadialTimePickerView.setAmOrPm(mInitialHourOfDay < 12 ? AM : PM); - mDelegator.invalidate(); - onTimeChanged(); + if (!is24HourView()) { + // convert [0,23] ordinal to wall clock display + if (currentHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + if (currentHour > HOURS_IN_HALF_DAY) { + currentHour = currentHour - HOURS_IN_HALF_DAY; + } + } else { + mIsAm = true; + if (currentHour == 0) { + currentHour = HOURS_IN_HALF_DAY; + } + } + updateAmPmControl(); + } + mHourSpinner.setValue(currentHour); + if (notifyTimeChanged) { + onTimeChanged(); + } } - /** - * @return The current hour in the range (0-23). - */ @Override public Integer getCurrentHour() { - int currentHour = mRadialTimePickerView.getCurrentHour(); - if (mIs24HourView) { + int currentHour = mHourSpinner.getValue(); + if (is24HourView()) { return currentHour; + } else if (mIsAm) { + return currentHour % HOURS_IN_HALF_DAY; } else { - switch(mRadialTimePickerView.getAmOrPm()) { - case PM: - return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; - case AM: - default: - return currentHour % HOURS_IN_HALF_DAY; - } + return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; } } - /** - * Set the current minute (0-59). - */ @Override public void setCurrentMinute(Integer currentMinute) { - if (mInitialMinute == currentMinute) { + if (currentMinute == getCurrentMinute()) { return; } - mInitialMinute = currentMinute; - updateHeaderMinute(currentMinute); - mRadialTimePickerView.setCurrentMinute(currentMinute); - mDelegator.invalidate(); + mMinuteSpinner.setValue(currentMinute); onTimeChanged(); } - /** - * @return The current minute. - */ @Override public Integer getCurrentMinute() { - return mRadialTimePickerView.getCurrentMinute(); + return mMinuteSpinner.getValue(); } - /** - * Set whether in 24 hour or AM/PM mode. - * - * @param is24HourView True = 24 hour mode. False = AM/PM. - */ @Override public void setIs24HourView(Boolean is24HourView) { - if (is24HourView == mIs24HourView) { + if (mIs24HourView == is24HourView) { return; } + // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!! + int currentHour = getCurrentHour(); + // Order is important here. mIs24HourView = is24HourView; - generateLegalTimesTree(); - int hour = mRadialTimePickerView.getCurrentHour(); - mInitialHourOfDay = hour; - updateHeaderHour(hour, false /* no accessibility announce */); - updateHeaderAmPm(); - updateRadialPicker(mRadialTimePickerView.getCurrentItemShowing()); - mDelegator.invalidate(); + getHourFormatData(); + updateHourControl(); + // set value after spinner range is updated + setCurrentHour(currentHour, false); + updateMinuteControl(); + updateAmPmControl(); } - /** - * @return true if this is in 24 hour view else false. - */ @Override public boolean is24HourView() { return mIs24HourView; } @Override - public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener callback) { - mOnTimeChangedListener = callback; + public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener) { + mOnTimeChangedListener = onTimeChangedListener; } @Override public void setEnabled(boolean enabled) { - mHourView.setEnabled(enabled); - mMinuteView.setEnabled(enabled); - mAmPmTextView.setEnabled(enabled); - mRadialTimePickerView.setEnabled(enabled); + mMinuteSpinner.setEnabled(enabled); + if (mDivider != null) { + mDivider.setEnabled(enabled); + } + mHourSpinner.setEnabled(enabled); + if (mAmPmSpinner != null) { + mAmPmSpinner.setEnabled(enabled); + } else { + mAmPmButton.setEnabled(enabled); + } mIsEnabled = enabled; } @@ -401,38 +386,24 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate im @Override public int getBaseline() { - // does not support baseline alignment - return -1; + return mHourSpinner.getBaseline(); } @Override public void onConfigurationChanged(Configuration newConfig) { - updateUI(mRadialTimePickerView.getCurrentItemShowing()); + setCurrentLocale(newConfig.locale); } @Override public Parcelable onSaveInstanceState(Parcelable superState) { - return new SavedState(superState, getCurrentHour(), getCurrentMinute(), - is24HourView(), inKbMode(), getTypedTimes(), getCurrentItemShowing()); + return new SavedState(superState, getCurrentHour(), getCurrentMinute()); } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; - setInKbMode(ss.inKbMode()); - setTypedTimes(ss.getTypesTimes()); - initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing()); - mRadialTimePickerView.invalidate(); - if (mInKbMode) { - tryStartingKbMode(-1); - mHourView.invalidate(); - } - } - - @Override - public void setCurrentLocale(Locale locale) { - super.setCurrentLocale(locale); - mTempCalendar = Calendar.getInstance(locale); + setCurrentHour(ss.getHour()); + setCurrentMinute(ss.getMinute()); } @Override @@ -451,9 +422,9 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate im } mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); - String selectedDate = DateUtils.formatDateTime(mContext, + String selectedDateUtterance = DateUtils.formatDateTime(mContext, mTempCalendar.getTimeInMillis(), flags); - event.getText().add(selectedDate); + event.getText().add(selectedDateUtterance); } @Override @@ -466,48 +437,121 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate im info.setClassName(TimePicker.class.getName()); } + private void updateInputState() { + // Make sure that if the user changes the value and the IME is active + // for one of the inputs if this widget, the IME is closed. If the user + // changed the value via the IME and there is a next input the IME will + // be shown, otherwise the user chose another means of changing the + // value and having the IME up makes no sense. + InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); + if (inputMethodManager != null) { + if (inputMethodManager.isActive(mHourSpinnerInput)) { + mHourSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) { + mMinuteSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) { + mAmPmSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } + } + } + + private void updateAmPmControl() { + if (is24HourView()) { + if (mAmPmSpinner != null) { + mAmPmSpinner.setVisibility(View.GONE); + } else { + mAmPmButton.setVisibility(View.GONE); + } + } else { + int index = mIsAm ? Calendar.AM : Calendar.PM; + if (mAmPmSpinner != null) { + mAmPmSpinner.setValue(index); + mAmPmSpinner.setVisibility(View.VISIBLE); + } else { + mAmPmButton.setText(mAmPmStrings[index]); + mAmPmButton.setVisibility(View.VISIBLE); + } + } + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + } + /** - * Set whether in keyboard mode or not. + * Sets the current locale. * - * @param inKbMode True means in keyboard mode. + * @param locale The current locale. */ - private void setInKbMode(boolean inKbMode) { - mInKbMode = inKbMode; + @Override + public void setCurrentLocale(Locale locale) { + super.setCurrentLocale(locale); + mTempCalendar = Calendar.getInstance(locale); } - /** - * @return true if in keyboard mode - */ - private boolean inKbMode() { - return mInKbMode; + private void onTimeChanged() { + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(), + getCurrentMinute()); + } } - private void setTypedTimes(ArrayList<Integer> typeTimes) { - mTypedTimes = typeTimes; + private void updateHourControl() { + if (is24HourView()) { + // 'k' means 1-24 hour + if (mHourFormat == 'k') { + mHourSpinner.setMinValue(1); + mHourSpinner.setMaxValue(24); + } else { + mHourSpinner.setMinValue(0); + mHourSpinner.setMaxValue(23); + } + } else { + // 'K' means 0-11 hour + if (mHourFormat == 'K') { + mHourSpinner.setMinValue(0); + mHourSpinner.setMaxValue(11); + } else { + mHourSpinner.setMinValue(1); + mHourSpinner.setMaxValue(12); + } + } + mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null); } - /** - * @return an array of typed times - */ - private ArrayList<Integer> getTypedTimes() { - return mTypedTimes; + private void updateMinuteControl() { + if (is24HourView()) { + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); + } else { + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + } } - /** - * @return the index of the current item showing - */ - private int getCurrentItemShowing() { - return mRadialTimePickerView.getCurrentItemShowing(); + private void setContentDescriptions() { + // Minute + trySetContentDescription(mMinuteSpinner, R.id.increment, + R.string.time_picker_increment_minute_button); + trySetContentDescription(mMinuteSpinner, R.id.decrement, + R.string.time_picker_decrement_minute_button); + // Hour + trySetContentDescription(mHourSpinner, R.id.increment, + R.string.time_picker_increment_hour_button); + trySetContentDescription(mHourSpinner, R.id.decrement, + R.string.time_picker_decrement_hour_button); + // AM/PM + if (mAmPmSpinner != null) { + trySetContentDescription(mAmPmSpinner, R.id.increment, + R.string.time_picker_increment_set_pm_button); + trySetContentDescription(mAmPmSpinner, R.id.decrement, + R.string.time_picker_decrement_set_am_button); + } } - /** - * Propagate the time change - */ - private void onTimeChanged() { - mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - if (mOnTimeChangedListener != null) { - mOnTimeChangedListener.onTimeChanged(mDelegator, - getCurrentHour(), getCurrentMinute()); + private void trySetContentDescription(View root, int viewId, int contDescResId) { + View target = root.findViewById(viewId); + if (target != null) { + target.setContentDescription(mContext.getString(contDescResId)); } } @@ -515,34 +559,19 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate im * Used to save / restore state of time picker */ private static class SavedState extends View.BaseSavedState { - private final int mHour; private final int mMinute; - private final boolean mIs24HourMode; - private final boolean mInKbMode; - private final ArrayList<Integer> mTypedTimes; - private final int mCurrentItemShowing; - - private SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode, - boolean isKbMode, ArrayList<Integer> typedTimes, - int currentItemShowing) { + + private SavedState(Parcelable superState, int hour, int minute) { super(superState); mHour = hour; mMinute = minute; - mIs24HourMode = is24HourMode; - mInKbMode = isKbMode; - mTypedTimes = typedTimes; - mCurrentItemShowing = currentItemShowing; } private SavedState(Parcel in) { super(in); mHour = in.readInt(); mMinute = in.readInt(); - mIs24HourMode = (in.readInt() == 1); - mInKbMode = (in.readInt() == 1); - mTypedTimes = in.readArrayList(getClass().getClassLoader()); - mCurrentItemShowing = in.readInt(); } public int getHour() { @@ -553,31 +582,11 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate im return mMinute; } - public boolean is24HourMode() { - return mIs24HourMode; - } - - public boolean inKbMode() { - return mInKbMode; - } - - public ArrayList<Integer> getTypesTimes() { - return mTypedTimes; - } - - public int getCurrentItemShowing() { - return mCurrentItemShowing; - } - @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(mHour); dest.writeInt(mMinute); - dest.writeInt(mIs24HourMode ? 1 : 0); - dest.writeInt(mInKbMode ? 1 : 0); - dest.writeList(mTypedTimes); - dest.writeInt(mCurrentItemShowing); } @SuppressWarnings({"unused", "hiding"}) @@ -592,667 +601,11 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate im }; } - private void tryVibrate() { - mDelegator.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); - } - - private void updateAmPmDisplay(int amOrPm) { - if (amOrPm == AM) { - mAmPmTextView.setText(mAmText); - mRadialTimePickerView.announceForAccessibility(mAmText); - } else if (amOrPm == PM){ - mAmPmTextView.setText(mPmText); - mRadialTimePickerView.announceForAccessibility(mPmText); - } else { - mAmPmTextView.setText(mDoublePlaceholderText); - } - } - - /** - * Called by the picker for updating the header display. - */ - @Override - public void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance) { - if (pickerIndex == HOUR_INDEX) { - updateHeaderHour(newValue, false); - String announcement = String.format("%d", newValue); - if (mAllowAutoAdvance && autoAdvance) { - setCurrentItemShowing(MINUTE_INDEX, true, false); - announcement += ". " + mSelectMinutes; - } else { - mRadialTimePickerView.setContentDescription( - mHourPickerDescription + ": " + newValue); - } - - mRadialTimePickerView.announceForAccessibility(announcement); - } else if (pickerIndex == MINUTE_INDEX){ - updateHeaderMinute(newValue); - mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + newValue); - } else if (pickerIndex == AMPM_INDEX) { - updateAmPmDisplay(newValue); - } else if (pickerIndex == ENABLE_PICKER_INDEX) { - if (!isTypedTimeFullyLegal()) { - mTypedTimes.clear(); - } - finishKbMode(); - } - } - - private void updateHeaderHour(int value, boolean announce) { - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, - (mIs24HourView) ? "Hm" : "hm"); - final int lengthPattern = bestDateTimePattern.length(); - boolean hourWithTwoDigit = false; - char hourFormat = '\0'; - // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save - // the hour format that we found. - for (int i = 0; i < lengthPattern; i++) { - final char c = bestDateTimePattern.charAt(i); - if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { - hourFormat = c; - if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { - hourWithTwoDigit = true; - } - break; - } - } - final String format; - if (hourWithTwoDigit) { - format = "%02d"; - } else { - format = "%d"; - } - if (mIs24HourView) { - // 'k' means 1-24 hour - if (hourFormat == 'k' && value == 0) { - value = 24; - } - } else { - // 'K' means 0-11 hour - value = modulo12(value, hourFormat == 'K'); - } - CharSequence text = String.format(format, value); - mHourView.setText(text); - if (announce) { - mRadialTimePickerView.announceForAccessibility(text); - } - } - - private static int modulo12(int n, boolean startWithZero) { - int value = n % 12; - if (value == 0 && !startWithZero) { - value = 12; - } - return value; - } - - /** - * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". - * - * See http://unicode.org/cldr/trac/browser/trunk/common/main - * - * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the - * separator as the character which is just after the hour marker in the returned pattern. - */ - private void updateHeaderSeparator() { - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, - (mIs24HourView) ? "Hm" : "hm"); - final String separatorText; - // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats - final char[] hourFormats = {'H', 'h', 'K', 'k'}; - int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats); - if (hIndex == -1) { - // Default case - separatorText = ":"; - } else { - separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1)); - } - mSeparatorView.setText(separatorText); - } - - static private int lastIndexOfAny(String str, char[] any) { - final int lengthAny = any.length; - if (lengthAny > 0) { - for (int i = str.length() - 1; i >= 0; i--) { - char c = str.charAt(i); - for (int j = 0; j < lengthAny; j++) { - if (c == any[j]) { - return i; - } - } - } - } - return -1; - } - - private void updateHeaderMinute(int value) { - if (value == 60) { - value = 0; - } - CharSequence text = String.format(mCurrentLocale, "%02d", value); - mRadialTimePickerView.announceForAccessibility(text); - mMinuteView.setText(text); - } - - /** - * Show either Hours or Minutes. - */ - private void setCurrentItemShowing(int index, boolean animateCircle, boolean announce) { - mRadialTimePickerView.setCurrentItemShowing(index, animateCircle); - - if (index == HOUR_INDEX) { - int hours = mRadialTimePickerView.getCurrentHour(); - if (!mIs24HourView) { - hours = hours % 12; - } - mRadialTimePickerView.setContentDescription(mHourPickerDescription + ": " + hours); - if (announce) { - mRadialTimePickerView.announceForAccessibility(mSelectHours); - } - } else { - int minutes = mRadialTimePickerView.getCurrentMinute(); - mRadialTimePickerView.setContentDescription(mMinutePickerDescription + ": " + minutes); - if (announce) { - mRadialTimePickerView.announceForAccessibility(mSelectMinutes); - } - } - - mHourView.setSelected(index == HOUR_INDEX); - mMinuteView.setSelected(index == MINUTE_INDEX); - } - - /** - * For keyboard mode, processes key events. - * - * @param keyCode the pressed key. - * - * @return true if the key was successfully processed, false otherwise. - */ - private boolean processKeyUp(int keyCode) { - if (keyCode == KeyEvent.KEYCODE_DEL) { - if (mInKbMode) { - if (!mTypedTimes.isEmpty()) { - int deleted = deleteLastTypedKey(); - String deletedKeyStr; - if (deleted == getAmOrPmKeyCode(AM)) { - deletedKeyStr = mAmText; - } else if (deleted == getAmOrPmKeyCode(PM)) { - deletedKeyStr = mPmText; - } else { - deletedKeyStr = String.format("%d", getValFromKeyCode(deleted)); - } - mRadialTimePickerView.announceForAccessibility( - String.format(mDeletedKeyFormat, deletedKeyStr)); - updateDisplay(true); - } - } - } else if (keyCode == KeyEvent.KEYCODE_0 || keyCode == KeyEvent.KEYCODE_1 - || keyCode == KeyEvent.KEYCODE_2 || keyCode == KeyEvent.KEYCODE_3 - || keyCode == KeyEvent.KEYCODE_4 || keyCode == KeyEvent.KEYCODE_5 - || keyCode == KeyEvent.KEYCODE_6 || keyCode == KeyEvent.KEYCODE_7 - || keyCode == KeyEvent.KEYCODE_8 || keyCode == KeyEvent.KEYCODE_9 - || (!mIs24HourView && - (keyCode == getAmOrPmKeyCode(AM) || keyCode == getAmOrPmKeyCode(PM)))) { - if (!mInKbMode) { - if (mRadialTimePickerView == null) { - // Something's wrong, because time picker should definitely not be null. - Log.e(TAG, "Unable to initiate keyboard mode, TimePicker was null."); - return true; - } - mTypedTimes.clear(); - tryStartingKbMode(keyCode); - return true; - } - // We're already in keyboard mode. - if (addKeyIfLegal(keyCode)) { - updateDisplay(false); - } - return true; - } - return false; - } - - /** - * Try to start keyboard mode with the specified key. - * - * @param keyCode The key to use as the first press. Keyboard mode will not be started if the - * key is not legal to start with. Or, pass in -1 to get into keyboard mode without a starting - * key. - */ - private void tryStartingKbMode(int keyCode) { - if (keyCode == -1 || addKeyIfLegal(keyCode)) { - mInKbMode = true; - onValidationChanged(false); - updateDisplay(false); - mRadialTimePickerView.setInputEnabled(false); - } - } - - private boolean addKeyIfLegal(int keyCode) { - // If we're in 24hour mode, we'll need to check if the input is full. If in AM/PM mode, - // we'll need to see if AM/PM have been typed. - if ((mIs24HourView && mTypedTimes.size() == 4) || - (!mIs24HourView && isTypedTimeFullyLegal())) { - return false; - } - - mTypedTimes.add(keyCode); - if (!isTypedTimeLegalSoFar()) { - deleteLastTypedKey(); - return false; - } - - int val = getValFromKeyCode(keyCode); - mRadialTimePickerView.announceForAccessibility(String.format("%d", val)); - // Automatically fill in 0's if AM or PM was legally entered. - if (isTypedTimeFullyLegal()) { - if (!mIs24HourView && mTypedTimes.size() <= 3) { - mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); - mTypedTimes.add(mTypedTimes.size() - 1, KeyEvent.KEYCODE_0); - } - onValidationChanged(true); - } - - return true; + public static String[] getAmPmStrings(Context context) { + String[] result = new String[2]; + LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale); + result[0] = d.amPm[0].length() > 2 ? d.narrowAm : d.amPm[0]; + result[1] = d.amPm[1].length() > 2 ? d.narrowPm : d.amPm[1]; + return result; } - - /** - * Traverse the tree to see if the keys that have been typed so far are legal as is, - * or may become legal as more keys are typed (excluding backspace). - */ - private boolean isTypedTimeLegalSoFar() { - Node node = mLegalTimesTree; - for (int keyCode : mTypedTimes) { - node = node.canReach(keyCode); - if (node == null) { - return false; - } - } - return true; - } - - /** - * Check if the time that has been typed so far is completely legal, as is. - */ - private boolean isTypedTimeFullyLegal() { - if (mIs24HourView) { - // For 24-hour mode, the time is legal if the hours and minutes are each legal. Note: - // getEnteredTime() will ONLY call isTypedTimeFullyLegal() when NOT in 24hour mode. - int[] values = getEnteredTime(null); - return (values[0] >= 0 && values[1] >= 0 && values[1] < 60); - } else { - // For AM/PM mode, the time is legal if it contains an AM or PM, as those can only be - // legally added at specific times based on the tree's algorithm. - return (mTypedTimes.contains(getAmOrPmKeyCode(AM)) || - mTypedTimes.contains(getAmOrPmKeyCode(PM))); - } - } - - private int deleteLastTypedKey() { - int deleted = mTypedTimes.remove(mTypedTimes.size() - 1); - if (!isTypedTimeFullyLegal()) { - onValidationChanged(false); - } - return deleted; - } - - /** - * Get out of keyboard mode. If there is nothing in typedTimes, revert to TimePicker's time. - */ - private void finishKbMode() { - mInKbMode = false; - if (!mTypedTimes.isEmpty()) { - int values[] = getEnteredTime(null); - mRadialTimePickerView.setCurrentHour(values[0]); - mRadialTimePickerView.setCurrentMinute(values[1]); - if (!mIs24HourView) { - mRadialTimePickerView.setAmOrPm(values[2]); - } - mTypedTimes.clear(); - } - updateDisplay(false); - mRadialTimePickerView.setInputEnabled(true); - } - - /** - * Update the hours, minutes, and AM/PM displays with the typed times. If the typedTimes is - * empty, either show an empty display (filled with the placeholder text), or update from the - * timepicker's values. - * - * @param allowEmptyDisplay if true, then if the typedTimes is empty, use the placeholder text. - * Otherwise, revert to the timepicker's values. - */ - private void updateDisplay(boolean allowEmptyDisplay) { - if (!allowEmptyDisplay && mTypedTimes.isEmpty()) { - int hour = mRadialTimePickerView.getCurrentHour(); - int minute = mRadialTimePickerView.getCurrentMinute(); - updateHeaderHour(hour, true); - updateHeaderMinute(minute); - if (!mIs24HourView) { - updateAmPmDisplay(hour < 12 ? AM : PM); - } - setCurrentItemShowing(mRadialTimePickerView.getCurrentItemShowing(), true, true); - onValidationChanged(true); - } else { - boolean[] enteredZeros = {false, false}; - int[] values = getEnteredTime(enteredZeros); - String hourFormat = enteredZeros[0] ? "%02d" : "%2d"; - String minuteFormat = (enteredZeros[1]) ? "%02d" : "%2d"; - String hourStr = (values[0] == -1) ? mDoublePlaceholderText : - String.format(hourFormat, values[0]).replace(' ', mPlaceholderText); - String minuteStr = (values[1] == -1) ? mDoublePlaceholderText : - String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText); - mHourView.setText(hourStr); - mHourView.setSelected(false); - mMinuteView.setText(minuteStr); - mMinuteView.setSelected(false); - if (!mIs24HourView) { - updateAmPmDisplay(values[2]); - } - } - } - - private int getValFromKeyCode(int keyCode) { - switch (keyCode) { - case KeyEvent.KEYCODE_0: - return 0; - case KeyEvent.KEYCODE_1: - return 1; - case KeyEvent.KEYCODE_2: - return 2; - case KeyEvent.KEYCODE_3: - return 3; - case KeyEvent.KEYCODE_4: - return 4; - case KeyEvent.KEYCODE_5: - return 5; - case KeyEvent.KEYCODE_6: - return 6; - case KeyEvent.KEYCODE_7: - return 7; - case KeyEvent.KEYCODE_8: - return 8; - case KeyEvent.KEYCODE_9: - return 9; - default: - return -1; - } - } - - /** - * Get the currently-entered time, as integer values of the hours and minutes typed. - * - * @param enteredZeros A size-2 boolean array, which the caller should initialize, and which - * may then be used for the caller to know whether zeros had been explicitly entered as either - * hours of minutes. This is helpful for deciding whether to show the dashes, or actual 0's. - * - * @return A size-3 int array. The first value will be the hours, the second value will be the - * minutes, and the third will be either AM or PM. - */ - private int[] getEnteredTime(boolean[] enteredZeros) { - int amOrPm = -1; - int startIndex = 1; - if (!mIs24HourView && isTypedTimeFullyLegal()) { - int keyCode = mTypedTimes.get(mTypedTimes.size() - 1); - if (keyCode == getAmOrPmKeyCode(AM)) { - amOrPm = AM; - } else if (keyCode == getAmOrPmKeyCode(PM)){ - amOrPm = PM; - } - startIndex = 2; - } - int minute = -1; - int hour = -1; - for (int i = startIndex; i <= mTypedTimes.size(); i++) { - int val = getValFromKeyCode(mTypedTimes.get(mTypedTimes.size() - i)); - if (i == startIndex) { - minute = val; - } else if (i == startIndex+1) { - minute += 10 * val; - if (enteredZeros != null && val == 0) { - enteredZeros[1] = true; - } - } else if (i == startIndex+2) { - hour = val; - } else if (i == startIndex+3) { - hour += 10 * val; - if (enteredZeros != null && val == 0) { - enteredZeros[0] = true; - } - } - } - - return new int[] { hour, minute, amOrPm }; - } - - /** - * Get the keycode value for AM and PM in the current language. - */ - private int getAmOrPmKeyCode(int amOrPm) { - // Cache the codes. - if (mAmKeyCode == -1 || mPmKeyCode == -1) { - // Find the first character in the AM/PM text that is unique. - KeyCharacterMap kcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); - char amChar; - char pmChar; - for (int i = 0; i < Math.max(mAmText.length(), mPmText.length()); i++) { - amChar = mAmText.toLowerCase(mCurrentLocale).charAt(i); - pmChar = mPmText.toLowerCase(mCurrentLocale).charAt(i); - if (amChar != pmChar) { - KeyEvent[] events = kcm.getEvents(new char[]{amChar, pmChar}); - // There should be 4 events: a down and up for both AM and PM. - if (events != null && events.length == 4) { - mAmKeyCode = events[0].getKeyCode(); - mPmKeyCode = events[2].getKeyCode(); - } else { - Log.e(TAG, "Unable to find keycodes for AM and PM."); - } - break; - } - } - } - if (amOrPm == AM) { - return mAmKeyCode; - } else if (amOrPm == PM) { - return mPmKeyCode; - } - - return -1; - } - - /** - * Create a tree for deciding what keys can legally be typed. - */ - private void generateLegalTimesTree() { - // Create a quick cache of numbers to their keycodes. - final int k0 = KeyEvent.KEYCODE_0; - final int k1 = KeyEvent.KEYCODE_1; - final int k2 = KeyEvent.KEYCODE_2; - final int k3 = KeyEvent.KEYCODE_3; - final int k4 = KeyEvent.KEYCODE_4; - final int k5 = KeyEvent.KEYCODE_5; - final int k6 = KeyEvent.KEYCODE_6; - final int k7 = KeyEvent.KEYCODE_7; - final int k8 = KeyEvent.KEYCODE_8; - final int k9 = KeyEvent.KEYCODE_9; - - // The root of the tree doesn't contain any numbers. - mLegalTimesTree = new Node(); - if (mIs24HourView) { - // We'll be re-using these nodes, so we'll save them. - Node minuteFirstDigit = new Node(k0, k1, k2, k3, k4, k5); - Node minuteSecondDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); - // The first digit must be followed by the second digit. - minuteFirstDigit.addChild(minuteSecondDigit); - - // The first digit may be 0-1. - Node firstDigit = new Node(k0, k1); - mLegalTimesTree.addChild(firstDigit); - - // When the first digit is 0-1, the second digit may be 0-5. - Node secondDigit = new Node(k0, k1, k2, k3, k4, k5); - firstDigit.addChild(secondDigit); - // We may now be followed by the first minute digit. E.g. 00:09, 15:58. - secondDigit.addChild(minuteFirstDigit); - - // When the first digit is 0-1, and the second digit is 0-5, the third digit may be 6-9. - Node thirdDigit = new Node(k6, k7, k8, k9); - // The time must now be finished. E.g. 0:55, 1:08. - secondDigit.addChild(thirdDigit); - - // When the first digit is 0-1, the second digit may be 6-9. - secondDigit = new Node(k6, k7, k8, k9); - firstDigit.addChild(secondDigit); - // We must now be followed by the first minute digit. E.g. 06:50, 18:20. - secondDigit.addChild(minuteFirstDigit); - - // The first digit may be 2. - firstDigit = new Node(k2); - mLegalTimesTree.addChild(firstDigit); - - // When the first digit is 2, the second digit may be 0-3. - secondDigit = new Node(k0, k1, k2, k3); - firstDigit.addChild(secondDigit); - // We must now be followed by the first minute digit. E.g. 20:50, 23:09. - secondDigit.addChild(minuteFirstDigit); - - // When the first digit is 2, the second digit may be 4-5. - secondDigit = new Node(k4, k5); - firstDigit.addChild(secondDigit); - // We must now be followd by the last minute digit. E.g. 2:40, 2:53. - secondDigit.addChild(minuteSecondDigit); - - // The first digit may be 3-9. - firstDigit = new Node(k3, k4, k5, k6, k7, k8, k9); - mLegalTimesTree.addChild(firstDigit); - // We must now be followed by the first minute digit. E.g. 3:57, 8:12. - firstDigit.addChild(minuteFirstDigit); - } else { - // We'll need to use the AM/PM node a lot. - // Set up AM and PM to respond to "a" and "p". - Node ampm = new Node(getAmOrPmKeyCode(AM), getAmOrPmKeyCode(PM)); - - // The first hour digit may be 1. - Node firstDigit = new Node(k1); - mLegalTimesTree.addChild(firstDigit); - // We'll allow quick input of on-the-hour times. E.g. 1pm. - firstDigit.addChild(ampm); - - // When the first digit is 1, the second digit may be 0-2. - Node secondDigit = new Node(k0, k1, k2); - firstDigit.addChild(secondDigit); - // Also for quick input of on-the-hour times. E.g. 10pm, 12am. - secondDigit.addChild(ampm); - - // When the first digit is 1, and the second digit is 0-2, the third digit may be 0-5. - Node thirdDigit = new Node(k0, k1, k2, k3, k4, k5); - secondDigit.addChild(thirdDigit); - // The time may be finished now. E.g. 1:02pm, 1:25am. - thirdDigit.addChild(ampm); - - // When the first digit is 1, the second digit is 0-2, and the third digit is 0-5, - // the fourth digit may be 0-9. - Node fourthDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); - thirdDigit.addChild(fourthDigit); - // The time must be finished now. E.g. 10:49am, 12:40pm. - fourthDigit.addChild(ampm); - - // When the first digit is 1, and the second digit is 0-2, the third digit may be 6-9. - thirdDigit = new Node(k6, k7, k8, k9); - secondDigit.addChild(thirdDigit); - // The time must be finished now. E.g. 1:08am, 1:26pm. - thirdDigit.addChild(ampm); - - // When the first digit is 1, the second digit may be 3-5. - secondDigit = new Node(k3, k4, k5); - firstDigit.addChild(secondDigit); - - // When the first digit is 1, and the second digit is 3-5, the third digit may be 0-9. - thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); - secondDigit.addChild(thirdDigit); - // The time must be finished now. E.g. 1:39am, 1:50pm. - thirdDigit.addChild(ampm); - - // The hour digit may be 2-9. - firstDigit = new Node(k2, k3, k4, k5, k6, k7, k8, k9); - mLegalTimesTree.addChild(firstDigit); - // We'll allow quick input of on-the-hour-times. E.g. 2am, 5pm. - firstDigit.addChild(ampm); - - // When the first digit is 2-9, the second digit may be 0-5. - secondDigit = new Node(k0, k1, k2, k3, k4, k5); - firstDigit.addChild(secondDigit); - - // When the first digit is 2-9, and the second digit is 0-5, the third digit may be 0-9. - thirdDigit = new Node(k0, k1, k2, k3, k4, k5, k6, k7, k8, k9); - secondDigit.addChild(thirdDigit); - // The time must be finished now. E.g. 2:57am, 9:30pm. - thirdDigit.addChild(ampm); - } - } - - /** - * Simple node class to be used for traversal to check for legal times. - * mLegalKeys represents the keys that can be typed to get to the node. - * mChildren are the children that can be reached from this node. - */ - private class Node { - private int[] mLegalKeys; - private ArrayList<Node> mChildren; - - public Node(int... legalKeys) { - mLegalKeys = legalKeys; - mChildren = new ArrayList<Node>(); - } - - public void addChild(Node child) { - mChildren.add(child); - } - - public boolean containsKey(int key) { - for (int i = 0; i < mLegalKeys.length; i++) { - if (mLegalKeys[i] == key) { - return true; - } - } - return false; - } - - public Node canReach(int key) { - if (mChildren == null) { - return null; - } - for (Node child : mChildren) { - if (child.containsKey(key)) { - return child; - } - } - return null; - } - } - - private final View.OnKeyListener mKeyListener = new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (event.getAction() == KeyEvent.ACTION_UP) { - return processKeyUp(keyCode); - } - return false; - } - }; - - private final View.OnFocusChangeListener mFocusListener = new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean hasFocus) { - if (!hasFocus && mInKbMode && isTypedTimeFullyLegal()) { - finishKbMode(); - - if (mOnTimeChangedListener != null) { - mOnTimeChangedListener.onTimeChanged(mDelegator, - mRadialTimePickerView.getCurrentHour(), - mRadialTimePickerView.getCurrentMinute()); - } - } - } - }; } |