| /* |
| * Copyright (C) 2013 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 static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; |
| import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; |
| |
| import android.annotation.TestApi; |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.os.Parcelable; |
| import android.text.format.DateFormat; |
| import android.text.format.DateUtils; |
| import android.util.AttributeSet; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.accessibility.AccessibilityEvent; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.InputMethodManager; |
| |
| import com.android.internal.R; |
| |
| import libcore.icu.LocaleData; |
| |
| import java.util.Calendar; |
| |
| /** |
| * A delegate implementing the basic spinner-based TimePicker. |
| */ |
| class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate { |
| private static final boolean DEFAULT_ENABLED_STATE = true; |
| private static final int HOURS_IN_HALF_DAY = 12; |
| |
| 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; |
| |
| // 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; |
| |
| private final String[] mAmPmStrings; |
| |
| private final Calendar mTempCalendar; |
| |
| private boolean mIsEnabled = DEFAULT_ENABLED_STATE; |
| private boolean mHourWithTwoDigit; |
| private char mHourFormat; |
| |
| private boolean mIs24HourView; |
| private boolean mIsAm; |
| |
| 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 int layoutResourceId = a.getResourceId( |
| R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy); |
| a.recycle(); |
| |
| final LayoutInflater inflater = LayoutInflater.from(mContext); |
| final View view = inflater.inflate(layoutResourceId, mDelegator, true); |
| view.setSaveFromParentEnabled(false); |
| |
| // hour |
| mHourSpinner = delegator.findViewById(R.id.hour); |
| mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { |
| public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { |
| updateInputState(); |
| if (!is24Hour()) { |
| 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 = mHourSpinner.findViewById(R.id.numberpicker_input); |
| mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); |
| |
| // divider (only for the new widget style) |
| mDivider = mDelegator.findViewById(R.id.divider); |
| if (mDivider != null) { |
| setDividerText(); |
| } |
| |
| // minute |
| mMinuteSpinner = 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 (!is24Hour() && newHour == HOURS_IN_HALF_DAY) { |
| mIsAm = !mIsAm; |
| updateAmPmControl(); |
| } |
| mHourSpinner.setValue(newHour); |
| } else if (oldVal == minValue && newVal == maxValue) { |
| int newHour = mHourSpinner.getValue() - 1; |
| if (!is24Hour() && newHour == HOURS_IN_HALF_DAY - 1) { |
| mIsAm = !mIsAm; |
| updateAmPmControl(); |
| } |
| mHourSpinner.setValue(newHour); |
| } |
| onTimeChanged(); |
| } |
| }); |
| mMinuteSpinnerInput = 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 = mAmPmSpinner.findViewById(R.id.numberpicker_input); |
| mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); |
| } |
| |
| if (isAmPmAtStart()) { |
| // Move the am/pm view to the beginning |
| ViewGroup amPmParent = 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); |
| } |
| } |
| |
| getHourFormatData(); |
| |
| // update controls to initial state |
| updateHourControl(); |
| updateMinuteControl(); |
| updateAmPmControl(); |
| |
| // set to current time |
| mTempCalendar = Calendar.getInstance(mLocale); |
| setHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); |
| setMinute(mTempCalendar.get(Calendar.MINUTE)); |
| |
| if (!isEnabled()) { |
| setEnabled(false); |
| } |
| |
| // set the content descriptions |
| setContentDescriptions(); |
| |
| // If not explicitly specified this view is important for accessibility. |
| if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { |
| mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); |
| } |
| } |
| |
| @Override |
| public boolean validateInput() { |
| return true; |
| } |
| |
| private void getHourFormatData() { |
| final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale, |
| (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 boolean isAmPmAtStart() { |
| final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale, |
| "hm" /* skeleton */); |
| |
| return bestDateTimePattern.startsWith("a"); |
| } |
| |
| /** |
| * 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(mLocale, |
| skeleton); |
| final String separatorText; |
| int hourIndex = bestDateTimePattern.lastIndexOf('H'); |
| if (hourIndex == -1) { |
| hourIndex = bestDateTimePattern.lastIndexOf('h'); |
| } |
| if (hourIndex == -1) { |
| // Default case |
| separatorText = ":"; |
| } else { |
| int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1); |
| if (minuteIndex == -1) { |
| separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1)); |
| } else { |
| separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex); |
| } |
| } |
| mDivider.setText(separatorText); |
| } |
| |
| @Override |
| public void setDate(int hour, int minute) { |
| setCurrentHour(hour, false); |
| setCurrentMinute(minute, false); |
| |
| onTimeChanged(); |
| } |
| |
| @Override |
| public void setHour(int hour) { |
| setCurrentHour(hour, true); |
| } |
| |
| private void setCurrentHour(int currentHour, boolean notifyTimeChanged) { |
| // why was Integer used in the first place? |
| if (currentHour == getHour()) { |
| return; |
| } |
| resetAutofilledValue(); |
| if (!is24Hour()) { |
| // 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(); |
| } |
| } |
| |
| @Override |
| public int getHour() { |
| int currentHour = mHourSpinner.getValue(); |
| if (is24Hour()) { |
| return currentHour; |
| } else if (mIsAm) { |
| return currentHour % HOURS_IN_HALF_DAY; |
| } else { |
| return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; |
| } |
| } |
| |
| @Override |
| public void setMinute(int minute) { |
| setCurrentMinute(minute, true); |
| } |
| |
| private void setCurrentMinute(int minute, boolean notifyTimeChanged) { |
| if (minute == getMinute()) { |
| return; |
| } |
| resetAutofilledValue(); |
| mMinuteSpinner.setValue(minute); |
| if (notifyTimeChanged) { |
| onTimeChanged(); |
| } |
| } |
| |
| @Override |
| public int getMinute() { |
| return mMinuteSpinner.getValue(); |
| } |
| |
| public void setIs24Hour(boolean is24Hour) { |
| if (mIs24HourView == is24Hour) { |
| return; |
| } |
| // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!! |
| int currentHour = getHour(); |
| // Order is important here. |
| mIs24HourView = is24Hour; |
| getHourFormatData(); |
| updateHourControl(); |
| // set value after spinner range is updated |
| setCurrentHour(currentHour, false); |
| updateMinuteControl(); |
| updateAmPmControl(); |
| } |
| |
| @Override |
| public boolean is24Hour() { |
| return mIs24HourView; |
| } |
| |
| @Override |
| public void setEnabled(boolean 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; |
| } |
| |
| @Override |
| public boolean isEnabled() { |
| return mIsEnabled; |
| } |
| |
| @Override |
| public int getBaseline() { |
| return mHourSpinner.getBaseline(); |
| } |
| |
| @Override |
| public Parcelable onSaveInstanceState(Parcelable superState) { |
| return new SavedState(superState, getHour(), getMinute(), is24Hour()); |
| } |
| |
| @Override |
| public void onRestoreInstanceState(Parcelable state) { |
| if (state instanceof SavedState) { |
| final SavedState ss = (SavedState) state; |
| setHour(ss.getHour()); |
| setMinute(ss.getMinute()); |
| } |
| } |
| |
| @Override |
| public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { |
| onPopulateAccessibilityEvent(event); |
| return true; |
| } |
| |
| @Override |
| public void onPopulateAccessibilityEvent(AccessibilityEvent event) { |
| int flags = DateUtils.FORMAT_SHOW_TIME; |
| if (mIs24HourView) { |
| flags |= DateUtils.FORMAT_24HOUR; |
| } else { |
| flags |= DateUtils.FORMAT_12HOUR; |
| } |
| mTempCalendar.set(Calendar.HOUR_OF_DAY, getHour()); |
| mTempCalendar.set(Calendar.MINUTE, getMinute()); |
| String selectedDateUtterance = DateUtils.formatDateTime(mContext, |
| mTempCalendar.getTimeInMillis(), flags); |
| event.getText().add(selectedDateUtterance); |
| } |
| |
| /** @hide */ |
| @Override |
| @TestApi |
| public View getHourView() { |
| return mHourSpinnerInput; |
| } |
| |
| /** @hide */ |
| @Override |
| @TestApi |
| public View getMinuteView() { |
| return mMinuteSpinnerInput; |
| } |
| |
| /** @hide */ |
| @Override |
| @TestApi |
| public View getAmView() { |
| return mAmPmSpinnerInput; |
| } |
| |
| /** @hide */ |
| @Override |
| @TestApi |
| public View getPmView() { |
| return mAmPmSpinnerInput; |
| } |
| |
| 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 (is24Hour()) { |
| 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); |
| } |
| |
| private void onTimeChanged() { |
| mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); |
| if (mOnTimeChangedListener != null) { |
| mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(), |
| getMinute()); |
| } |
| if (mAutoFillChangeListener != null) { |
| mAutoFillChangeListener.onTimeChanged(mDelegator, getHour(), getMinute()); |
| } |
| } |
| |
| private void updateHourControl() { |
| if (is24Hour()) { |
| // '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); |
| } |
| |
| private void updateMinuteControl() { |
| if (is24Hour()) { |
| mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); |
| } else { |
| mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); |
| } |
| } |
| |
| 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); |
| } |
| } |
| |
| private void trySetContentDescription(View root, int viewId, int contDescResId) { |
| View target = root.findViewById(viewId); |
| if (target != null) { |
| target.setContentDescription(mContext.getString(contDescResId)); |
| } |
| } |
| |
| 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() > 4 ? d.narrowAm : d.amPm[0]; |
| result[1] = d.amPm[1].length() > 4 ? d.narrowPm : d.amPm[1]; |
| return result; |
| } |
| } |