From 3053b2fdcf7486f2e2f572f9b05ce65dacdd2b4c Mon Sep 17 00:00:00 2001 From: Chet Haase Date: Wed, 6 Aug 2014 07:51:50 -0700 Subject: Allow TimePicker/DatePicker to use legacy layout style In L, the TimePicker was rewritten to use a clock face and the DatePicker was rewritten to use a calendar. This change allows developers to opt into the old style instead (android:timePickerMode="spinner|clock" and android:datePickerMode="spinner|calendar").) Issue #16717952 Pre-L style android.widget.TimePicker on L Issue #16846963 Allow developer to use legacy DatePicker appearance Change-Id: I4ee071fcbc5bf25eab83d6a5cb6ff352a603f90a --- .../android/widget/TimePickerClockDelegate.java | 684 +++++++++++++++++++++ 1 file changed, 684 insertions(+) create mode 100644 core/java/android/widget/TimePickerClockDelegate.java (limited to 'core/java/android/widget/TimePickerClockDelegate.java') diff --git a/core/java/android/widget/TimePickerClockDelegate.java b/core/java/android/widget/TimePickerClockDelegate.java new file mode 100644 index 0000000..59ec6d3 --- /dev/null +++ b/core/java/android/widget/TimePickerClockDelegate.java @@ -0,0 +1,684 @@ +/* + * 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 android.content.Context; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.os.Parcel; +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.accessibility.AccessibilityNodeInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import com.android.internal.R; + +import java.text.DateFormatSymbols; +import java.util.Calendar; +import java.util.Locale; + +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; + +/** + * A delegate implementing the basic TimePicker + */ +class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate { + + private static final boolean DEFAULT_ENABLED_STATE = true; + + private static final int HOURS_IN_HALF_DAY = 12; + + // state + private boolean mIs24HourView; + + private boolean mIsAm; + + // 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; + + // 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; + + // May be null if layout has no done button + private final View mDoneButton; + private boolean mShowDoneButton; + private TimePicker.TimePickerDismissCallback mDismissCallback; + + private final String[] mAmPmStrings; + + private boolean mIsEnabled = DEFAULT_ENABLED_STATE; + + private Calendar mTempCalendar; + + private boolean mHourWithTwoDigit; + private char mHourFormat; + + /** + * A no-op callback used in the constructor to avoid null checks later in + * the code. + */ + private static final TimePicker.OnTimeChangedListener NO_OP_CHANGE_LISTENER = + new TimePicker.OnTimeChangedListener() { + public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { + } + }; + + public TimePickerClockDelegate(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); + 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 = new DateFormatSymbols().getAmPmStrings(); + + // am/pm + 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); + } + } + + mDoneButton = delegator.findViewById(R.id.done_button); + mShowDoneButton = (mDoneButton != null); + if (mShowDoneButton) { + mDoneButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mDismissCallback != null) { + mDismissCallback.dismiss(mDelegator, false, getCurrentHour(), + getCurrentMinute()); + } + } + }); + } + + getHourFormatData(); + + // update controls to initial state + updateHourControl(); + updateMinuteControl(); + updateAmPmControl(); + + setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); + + // set to current time + setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); + setCurrentMinute(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); + } + + updateDoneButton(); + } + + 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 boolean isAmPmAtStart() { + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale, + "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(mCurrentLocale, + 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 setCurrentHour(Integer 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; + } + 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(); + } + } + + @Override + public Integer getCurrentHour() { + int currentHour = mHourSpinner.getValue(); + if (is24HourView()) { + 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 setCurrentMinute(Integer currentMinute) { + if (currentMinute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(currentMinute); + onTimeChanged(); + } + + @Override + public Integer getCurrentMinute() { + return mMinuteSpinner.getValue(); + } + + @Override + public void setIs24HourView(Boolean is24HourView) { + 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; + getHourFormatData(); + updateHourControl(); + // set value after spinner range is updated + setCurrentHour(currentHour, false); + updateMinuteControl(); + updateAmPmControl(); + } + + @Override + public boolean is24HourView() { + return mIs24HourView; + } + + @Override + public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener) { + mOnTimeChangedListener = onTimeChangedListener; + } + + @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 void setShowDoneButton(boolean showDoneButton) { + if (mDoneButton != null) { + mShowDoneButton = showDoneButton; + updateDoneButton(); + } + } + + @Override + public boolean isShowDoneButton() { + return mShowDoneButton; + } + + private void updateDoneButton() { + if (mDoneButton != null) { + mDoneButton.setVisibility(mShowDoneButton ? View.VISIBLE : View.GONE); + } + } + + @Override + public void setDismissCallback(TimePicker.TimePickerDismissCallback callback) { + mDismissCallback = callback; + } + + @Override + public int getBaseline() { + return mHourSpinner.getBaseline(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + setCurrentLocale(newConfig.locale); + } + + @Override + public Parcelable onSaveInstanceState(Parcelable superState) { + return new SavedState(superState, getCurrentHour(), getCurrentMinute(), + isShowDoneButton()); + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + setCurrentHour(ss.getHour()); + setCurrentMinute(ss.getMinute()); + setShowDoneButton(ss.isShowDoneButton()); + } + + @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, getCurrentHour()); + mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); + String selectedDateUtterance = DateUtils.formatDateTime(mContext, + mTempCalendar.getTimeInMillis(), flags); + event.getText().add(selectedDateUtterance); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + event.setClassName(TimePicker.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + 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); + } + + /** + * Sets the current locale. + * + * @param locale The current locale. + */ + @Override + public void setCurrentLocale(Locale locale) { + super.setCurrentLocale(locale); + mTempCalendar = Calendar.getInstance(locale); + } + + private void onTimeChanged() { + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(), + getCurrentMinute()); + } + } + + 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); + } + + private void updateMinuteControl() { + if (is24HourView()) { + 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)); + } + } + + /** + * 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 mShowDoneButton; + + private SavedState(Parcelable superState, int hour, int minute, boolean showDoneButton) { + super(superState); + mHour = hour; + mMinute = minute; + mShowDoneButton = showDoneButton; + } + + private SavedState(Parcel in) { + super(in); + mHour = in.readInt(); + mMinute = in.readInt(); + mShowDoneButton = (in.readInt() == 1); + } + + public int getHour() { + return mHour; + } + + public int getMinute() { + return mMinute; + } + + public boolean isShowDoneButton() { + return mShowDoneButton; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mHour); + dest.writeInt(mMinute); + dest.writeInt(mShowDoneButton ? 1 : 0); + } + + @SuppressWarnings({"unused", "hiding"}) + public static final Parcelable.Creator CREATOR = new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} + -- cgit v1.1