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