Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import android.content.Context;
     20 import android.content.res.Configuration;
     21 import android.content.res.TypedArray;
     22 import android.os.Parcel;
     23 import android.os.Parcelable;
     24 import android.text.format.DateFormat;
     25 import android.text.format.DateUtils;
     26 import android.util.AttributeSet;
     27 import android.view.LayoutInflater;
     28 import android.view.View;
     29 import android.view.ViewGroup;
     30 import android.view.accessibility.AccessibilityEvent;
     31 import android.view.inputmethod.EditorInfo;
     32 import android.view.inputmethod.InputMethodManager;
     33 import com.android.internal.R;
     34 
     35 import java.util.Calendar;
     36 import java.util.Locale;
     37 
     38 import libcore.icu.LocaleData;
     39 
     40 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
     41 import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
     42 
     43 /**
     44  * A delegate implementing the basic spinner-based TimePicker.
     45  */
     46 class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate {
     47     private static final boolean DEFAULT_ENABLED_STATE = true;
     48     private static final int HOURS_IN_HALF_DAY = 12;
     49 
     50     // state
     51     private boolean mIs24HourView;
     52     private boolean mIsAm;
     53 
     54     // ui components
     55     private final NumberPicker mHourSpinner;
     56     private final NumberPicker mMinuteSpinner;
     57     private final NumberPicker mAmPmSpinner;
     58     private final EditText mHourSpinnerInput;
     59     private final EditText mMinuteSpinnerInput;
     60     private final EditText mAmPmSpinnerInput;
     61     private final TextView mDivider;
     62 
     63     // Note that the legacy implementation of the TimePicker is
     64     // using a button for toggling between AM/PM while the new
     65     // version uses a NumberPicker spinner. Therefore the code
     66     // accommodates these two cases to be backwards compatible.
     67     private final Button mAmPmButton;
     68 
     69     private final String[] mAmPmStrings;
     70 
     71     private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
     72     private Calendar mTempCalendar;
     73     private boolean mHourWithTwoDigit;
     74     private char mHourFormat;
     75 
     76     public TimePickerSpinnerDelegate(TimePicker delegator, Context context, AttributeSet attrs,
     77             int defStyleAttr, int defStyleRes) {
     78         super(delegator, context);
     79 
     80         // process style attributes
     81         final TypedArray a = mContext.obtainStyledAttributes(
     82                 attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
     83         final int layoutResourceId = a.getResourceId(
     84                 R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy);
     85         a.recycle();
     86 
     87         final LayoutInflater inflater = LayoutInflater.from(mContext);
     88         inflater.inflate(layoutResourceId, mDelegator, true);
     89 
     90         // hour
     91         mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour);
     92         mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
     93             public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
     94                 updateInputState();
     95                 if (!is24HourView()) {
     96                     if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) ||
     97                             (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) {
     98                         mIsAm = !mIsAm;
     99                         updateAmPmControl();
    100                     }
    101                 }
    102                 onTimeChanged();
    103             }
    104         });
    105         mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input);
    106         mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
    107 
    108         // divider (only for the new widget style)
    109         mDivider = (TextView) mDelegator.findViewById(R.id.divider);
    110         if (mDivider != null) {
    111             setDividerText();
    112         }
    113 
    114         // minute
    115         mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute);
    116         mMinuteSpinner.setMinValue(0);
    117         mMinuteSpinner.setMaxValue(59);
    118         mMinuteSpinner.setOnLongPressUpdateInterval(100);
    119         mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
    120         mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
    121             public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
    122                 updateInputState();
    123                 int minValue = mMinuteSpinner.getMinValue();
    124                 int maxValue = mMinuteSpinner.getMaxValue();
    125                 if (oldVal == maxValue && newVal == minValue) {
    126                     int newHour = mHourSpinner.getValue() + 1;
    127                     if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) {
    128                         mIsAm = !mIsAm;
    129                         updateAmPmControl();
    130                     }
    131                     mHourSpinner.setValue(newHour);
    132                 } else if (oldVal == minValue && newVal == maxValue) {
    133                     int newHour = mHourSpinner.getValue() - 1;
    134                     if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) {
    135                         mIsAm = !mIsAm;
    136                         updateAmPmControl();
    137                     }
    138                     mHourSpinner.setValue(newHour);
    139                 }
    140                 onTimeChanged();
    141             }
    142         });
    143         mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input);
    144         mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
    145 
    146         // Get the localized am/pm strings and use them in the spinner.
    147         mAmPmStrings = getAmPmStrings(context);
    148 
    149         // am/pm
    150         final View amPmView = mDelegator.findViewById(R.id.amPm);
    151         if (amPmView instanceof Button) {
    152             mAmPmSpinner = null;
    153             mAmPmSpinnerInput = null;
    154             mAmPmButton = (Button) amPmView;
    155             mAmPmButton.setOnClickListener(new View.OnClickListener() {
    156                 public void onClick(View button) {
    157                     button.requestFocus();
    158                     mIsAm = !mIsAm;
    159                     updateAmPmControl();
    160                     onTimeChanged();
    161                 }
    162             });
    163         } else {
    164             mAmPmButton = null;
    165             mAmPmSpinner = (NumberPicker) amPmView;
    166             mAmPmSpinner.setMinValue(0);
    167             mAmPmSpinner.setMaxValue(1);
    168             mAmPmSpinner.setDisplayedValues(mAmPmStrings);
    169             mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
    170                 public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
    171                     updateInputState();
    172                     picker.requestFocus();
    173                     mIsAm = !mIsAm;
    174                     updateAmPmControl();
    175                     onTimeChanged();
    176                 }
    177             });
    178             mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input);
    179             mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
    180         }
    181 
    182         if (isAmPmAtStart()) {
    183             // Move the am/pm view to the beginning
    184             ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout);
    185             amPmParent.removeView(amPmView);
    186             amPmParent.addView(amPmView, 0);
    187             // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme
    188             // for example and not for Holo Theme)
    189             ViewGroup.MarginLayoutParams lp =
    190                     (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams();
    191             final int startMargin = lp.getMarginStart();
    192             final int endMargin = lp.getMarginEnd();
    193             if (startMargin != endMargin) {
    194                 lp.setMarginStart(endMargin);
    195                 lp.setMarginEnd(startMargin);
    196             }
    197         }
    198 
    199         getHourFormatData();
    200 
    201         // update controls to initial state
    202         updateHourControl();
    203         updateMinuteControl();
    204         updateAmPmControl();
    205 
    206         // set to current time
    207         setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY));
    208         setCurrentMinute(mTempCalendar.get(Calendar.MINUTE));
    209 
    210         if (!isEnabled()) {
    211             setEnabled(false);
    212         }
    213 
    214         // set the content descriptions
    215         setContentDescriptions();
    216 
    217         // If not explicitly specified this view is important for accessibility.
    218         if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
    219             mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    220         }
    221     }
    222 
    223     private void getHourFormatData() {
    224         final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
    225                 (mIs24HourView) ? "Hm" : "hm");
    226         final int lengthPattern = bestDateTimePattern.length();
    227         mHourWithTwoDigit = false;
    228         char hourFormat = '\0';
    229         // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save
    230         // the hour format that we found.
    231         for (int i = 0; i < lengthPattern; i++) {
    232             final char c = bestDateTimePattern.charAt(i);
    233             if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
    234                 mHourFormat = c;
    235                 if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
    236                     mHourWithTwoDigit = true;
    237                 }
    238                 break;
    239             }
    240         }
    241     }
    242 
    243     private boolean isAmPmAtStart() {
    244         final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
    245                 "hm" /* skeleton */);
    246 
    247         return bestDateTimePattern.startsWith("a");
    248     }
    249 
    250     /**
    251      * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
    252      *
    253      * See http://unicode.org/cldr/trac/browser/trunk/common/main
    254      *
    255      * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the
    256      * separator as the character which is just after the hour marker in the returned pattern.
    257      */
    258     private void setDividerText() {
    259         final String skeleton = (mIs24HourView) ? "Hm" : "hm";
    260         final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mCurrentLocale,
    261                 skeleton);
    262         final String separatorText;
    263         int hourIndex = bestDateTimePattern.lastIndexOf('H');
    264         if (hourIndex == -1) {
    265             hourIndex = bestDateTimePattern.lastIndexOf('h');
    266         }
    267         if (hourIndex == -1) {
    268             // Default case
    269             separatorText = ":";
    270         } else {
    271             int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1);
    272             if  (minuteIndex == -1) {
    273                 separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1));
    274             } else {
    275                 separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex);
    276             }
    277         }
    278         mDivider.setText(separatorText);
    279     }
    280 
    281     @Override
    282     public void setCurrentHour(int currentHour) {
    283         setCurrentHour(currentHour, true);
    284     }
    285 
    286     private void setCurrentHour(int currentHour, boolean notifyTimeChanged) {
    287         // why was Integer used in the first place?
    288         if (currentHour == getCurrentHour()) {
    289             return;
    290         }
    291         if (!is24HourView()) {
    292             // convert [0,23] ordinal to wall clock display
    293             if (currentHour >= HOURS_IN_HALF_DAY) {
    294                 mIsAm = false;
    295                 if (currentHour > HOURS_IN_HALF_DAY) {
    296                     currentHour = currentHour - HOURS_IN_HALF_DAY;
    297                 }
    298             } else {
    299                 mIsAm = true;
    300                 if (currentHour == 0) {
    301                     currentHour = HOURS_IN_HALF_DAY;
    302                 }
    303             }
    304             updateAmPmControl();
    305         }
    306         mHourSpinner.setValue(currentHour);
    307         if (notifyTimeChanged) {
    308             onTimeChanged();
    309         }
    310     }
    311 
    312     @Override
    313     public int getCurrentHour() {
    314         int currentHour = mHourSpinner.getValue();
    315         if (is24HourView()) {
    316             return currentHour;
    317         } else if (mIsAm) {
    318             return currentHour % HOURS_IN_HALF_DAY;
    319         } else {
    320             return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
    321         }
    322     }
    323 
    324     @Override
    325     public void setCurrentMinute(int currentMinute) {
    326         if (currentMinute == getCurrentMinute()) {
    327             return;
    328         }
    329         mMinuteSpinner.setValue(currentMinute);
    330         onTimeChanged();
    331     }
    332 
    333     @Override
    334     public int getCurrentMinute() {
    335         return mMinuteSpinner.getValue();
    336     }
    337 
    338     @Override
    339     public void setIs24HourView(boolean is24HourView) {
    340         if (mIs24HourView == is24HourView) {
    341             return;
    342         }
    343         // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!!
    344         int currentHour = getCurrentHour();
    345         // Order is important here.
    346         mIs24HourView = is24HourView;
    347         getHourFormatData();
    348         updateHourControl();
    349         // set value after spinner range is updated
    350         setCurrentHour(currentHour, false);
    351         updateMinuteControl();
    352         updateAmPmControl();
    353     }
    354 
    355     @Override
    356     public boolean is24HourView() {
    357         return mIs24HourView;
    358     }
    359 
    360     @Override
    361     public void setOnTimeChangedListener(TimePicker.OnTimeChangedListener onTimeChangedListener) {
    362         mOnTimeChangedListener = onTimeChangedListener;
    363     }
    364 
    365     @Override
    366     public void setEnabled(boolean enabled) {
    367         mMinuteSpinner.setEnabled(enabled);
    368         if (mDivider != null) {
    369             mDivider.setEnabled(enabled);
    370         }
    371         mHourSpinner.setEnabled(enabled);
    372         if (mAmPmSpinner != null) {
    373             mAmPmSpinner.setEnabled(enabled);
    374         } else {
    375             mAmPmButton.setEnabled(enabled);
    376         }
    377         mIsEnabled = enabled;
    378     }
    379 
    380     @Override
    381     public boolean isEnabled() {
    382         return mIsEnabled;
    383     }
    384 
    385     @Override
    386     public int getBaseline() {
    387         return mHourSpinner.getBaseline();
    388     }
    389 
    390     @Override
    391     public void onConfigurationChanged(Configuration newConfig) {
    392         setCurrentLocale(newConfig.locale);
    393     }
    394 
    395     @Override
    396     public Parcelable onSaveInstanceState(Parcelable superState) {
    397         return new SavedState(superState, getCurrentHour(), getCurrentMinute());
    398     }
    399 
    400     @Override
    401     public void onRestoreInstanceState(Parcelable state) {
    402         SavedState ss = (SavedState) state;
    403         setCurrentHour(ss.getHour());
    404         setCurrentMinute(ss.getMinute());
    405     }
    406 
    407     @Override
    408     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    409         onPopulateAccessibilityEvent(event);
    410         return true;
    411     }
    412 
    413     @Override
    414     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    415         int flags = DateUtils.FORMAT_SHOW_TIME;
    416         if (mIs24HourView) {
    417             flags |= DateUtils.FORMAT_24HOUR;
    418         } else {
    419             flags |= DateUtils.FORMAT_12HOUR;
    420         }
    421         mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour());
    422         mTempCalendar.set(Calendar.MINUTE, getCurrentMinute());
    423         String selectedDateUtterance = DateUtils.formatDateTime(mContext,
    424                 mTempCalendar.getTimeInMillis(), flags);
    425         event.getText().add(selectedDateUtterance);
    426     }
    427 
    428     private void updateInputState() {
    429         // Make sure that if the user changes the value and the IME is active
    430         // for one of the inputs if this widget, the IME is closed. If the user
    431         // changed the value via the IME and there is a next input the IME will
    432         // be shown, otherwise the user chose another means of changing the
    433         // value and having the IME up makes no sense.
    434         InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
    435         if (inputMethodManager != null) {
    436             if (inputMethodManager.isActive(mHourSpinnerInput)) {
    437                 mHourSpinnerInput.clearFocus();
    438                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
    439             } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) {
    440                 mMinuteSpinnerInput.clearFocus();
    441                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
    442             } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) {
    443                 mAmPmSpinnerInput.clearFocus();
    444                 inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
    445             }
    446         }
    447     }
    448 
    449     private void updateAmPmControl() {
    450         if (is24HourView()) {
    451             if (mAmPmSpinner != null) {
    452                 mAmPmSpinner.setVisibility(View.GONE);
    453             } else {
    454                 mAmPmButton.setVisibility(View.GONE);
    455             }
    456         } else {
    457             int index = mIsAm ? Calendar.AM : Calendar.PM;
    458             if (mAmPmSpinner != null) {
    459                 mAmPmSpinner.setValue(index);
    460                 mAmPmSpinner.setVisibility(View.VISIBLE);
    461             } else {
    462                 mAmPmButton.setText(mAmPmStrings[index]);
    463                 mAmPmButton.setVisibility(View.VISIBLE);
    464             }
    465         }
    466         mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
    467     }
    468 
    469     /**
    470      * Sets the current locale.
    471      *
    472      * @param locale The current locale.
    473      */
    474     @Override
    475     public void setCurrentLocale(Locale locale) {
    476         super.setCurrentLocale(locale);
    477         mTempCalendar = Calendar.getInstance(locale);
    478     }
    479 
    480     private void onTimeChanged() {
    481         mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
    482         if (mOnTimeChangedListener != null) {
    483             mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(),
    484                     getCurrentMinute());
    485         }
    486     }
    487 
    488     private void updateHourControl() {
    489         if (is24HourView()) {
    490             // 'k' means 1-24 hour
    491             if (mHourFormat == 'k') {
    492                 mHourSpinner.setMinValue(1);
    493                 mHourSpinner.setMaxValue(24);
    494             } else {
    495                 mHourSpinner.setMinValue(0);
    496                 mHourSpinner.setMaxValue(23);
    497             }
    498         } else {
    499             // 'K' means 0-11 hour
    500             if (mHourFormat == 'K') {
    501                 mHourSpinner.setMinValue(0);
    502                 mHourSpinner.setMaxValue(11);
    503             } else {
    504                 mHourSpinner.setMinValue(1);
    505                 mHourSpinner.setMaxValue(12);
    506             }
    507         }
    508         mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null);
    509     }
    510 
    511     private void updateMinuteControl() {
    512         if (is24HourView()) {
    513             mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
    514         } else {
    515             mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
    516         }
    517     }
    518 
    519     private void setContentDescriptions() {
    520         // Minute
    521         trySetContentDescription(mMinuteSpinner, R.id.increment,
    522                 R.string.time_picker_increment_minute_button);
    523         trySetContentDescription(mMinuteSpinner, R.id.decrement,
    524                 R.string.time_picker_decrement_minute_button);
    525         // Hour
    526         trySetContentDescription(mHourSpinner, R.id.increment,
    527                 R.string.time_picker_increment_hour_button);
    528         trySetContentDescription(mHourSpinner, R.id.decrement,
    529                 R.string.time_picker_decrement_hour_button);
    530         // AM/PM
    531         if (mAmPmSpinner != null) {
    532             trySetContentDescription(mAmPmSpinner, R.id.increment,
    533                     R.string.time_picker_increment_set_pm_button);
    534             trySetContentDescription(mAmPmSpinner, R.id.decrement,
    535                     R.string.time_picker_decrement_set_am_button);
    536         }
    537     }
    538 
    539     private void trySetContentDescription(View root, int viewId, int contDescResId) {
    540         View target = root.findViewById(viewId);
    541         if (target != null) {
    542             target.setContentDescription(mContext.getString(contDescResId));
    543         }
    544     }
    545 
    546     /**
    547      * Used to save / restore state of time picker
    548      */
    549     private static class SavedState extends View.BaseSavedState {
    550         private final int mHour;
    551         private final int mMinute;
    552 
    553         private SavedState(Parcelable superState, int hour, int minute) {
    554             super(superState);
    555             mHour = hour;
    556             mMinute = minute;
    557         }
    558 
    559         private SavedState(Parcel in) {
    560             super(in);
    561             mHour = in.readInt();
    562             mMinute = in.readInt();
    563         }
    564 
    565         public int getHour() {
    566             return mHour;
    567         }
    568 
    569         public int getMinute() {
    570             return mMinute;
    571         }
    572 
    573         @Override
    574         public void writeToParcel(Parcel dest, int flags) {
    575             super.writeToParcel(dest, flags);
    576             dest.writeInt(mHour);
    577             dest.writeInt(mMinute);
    578         }
    579 
    580         @SuppressWarnings({"unused", "hiding"})
    581         public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
    582             public SavedState createFromParcel(Parcel in) {
    583                 return new SavedState(in);
    584             }
    585 
    586             public SavedState[] newArray(int size) {
    587                 return new SavedState[size];
    588             }
    589         };
    590     }
    591 
    592     public static String[] getAmPmStrings(Context context) {
    593         String[] result = new String[2];
    594         LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
    595         result[0] = d.amPm[0].length() > 4 ? d.narrowAm : d.amPm[0];
    596         result[1] = d.amPm[1].length() > 4 ? d.narrowPm : d.amPm[1];
    597         return result;
    598     }
    599 }
    600