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