Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2007 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.annotation.Widget;
     20 import android.content.Context;
     21 import android.content.res.Configuration;
     22 import android.content.res.TypedArray;
     23 import android.os.Parcel;
     24 import android.os.Parcelable;
     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.accessibility.AccessibilityEvent;
     30 import android.view.accessibility.AccessibilityNodeInfo;
     31 import android.view.inputmethod.EditorInfo;
     32 import android.view.inputmethod.InputMethodManager;
     33 import android.widget.NumberPicker.OnValueChangeListener;
     34 
     35 import com.android.internal.R;
     36 
     37 import java.text.DateFormatSymbols;
     38 import java.util.Calendar;
     39 import java.util.Locale;
     40 
     41 /**
     42  * A view for selecting the time of day, in either 24 hour or AM/PM mode. The
     43  * hour, each minute digit, and AM/PM (if applicable) can be conrolled by
     44  * vertical spinners. The hour can be entered by keyboard input. Entering in two
     45  * digit hours can be accomplished by hitting two digits within a timeout of
     46  * about a second (e.g. '1' then '2' to select 12). The minutes can be entered
     47  * by entering single digits. Under AM/PM mode, the user can hit 'a', 'A", 'p'
     48  * or 'P' to pick. For a dialog using this view, see
     49  * {@link android.app.TimePickerDialog}.
     50  *<p>
     51  * See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
     52  * guide.
     53  * </p>
     54  */
     55 @Widget
     56 public class TimePicker extends FrameLayout {
     57 
     58     private static final boolean DEFAULT_ENABLED_STATE = true;
     59 
     60     private static final int HOURS_IN_HALF_DAY = 12;
     61 
     62     /**
     63      * A no-op callback used in the constructor to avoid null checks later in
     64      * the code.
     65      */
     66     private static final OnTimeChangedListener NO_OP_CHANGE_LISTENER = new OnTimeChangedListener() {
     67         public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
     68         }
     69     };
     70 
     71     // state
     72     private boolean mIs24HourView;
     73 
     74     private boolean mIsAm;
     75 
     76     // ui components
     77     private final NumberPicker mHourSpinner;
     78 
     79     private final NumberPicker mMinuteSpinner;
     80 
     81     private final NumberPicker mAmPmSpinner;
     82 
     83     private final EditText mHourSpinnerInput;
     84 
     85     private final EditText mMinuteSpinnerInput;
     86 
     87     private final EditText mAmPmSpinnerInput;
     88 
     89     private final TextView mDivider;
     90 
     91     // Note that the legacy implementation of the TimePicker is
     92     // using a button for toggling between AM/PM while the new
     93     // version uses a NumberPicker spinner. Therefore the code
     94     // accommodates these two cases to be backwards compatible.
     95     private final Button mAmPmButton;
     96 
     97     private final String[] mAmPmStrings;
     98 
     99     private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
    100 
    101     // callbacks
    102     private OnTimeChangedListener mOnTimeChangedListener;
    103 
    104     private Calendar mTempCalendar;
    105 
    106     private Locale mCurrentLocale;
    107 
    108     /**
    109      * The callback interface used to indicate the time has been adjusted.
    110      */
    111     public interface OnTimeChangedListener {
    112 
    113         /**
    114          * @param view The view associated with this listener.
    115          * @param hourOfDay The current hour.
    116          * @param minute The current minute.
    117          */
    118         void onTimeChanged(TimePicker view, int hourOfDay, int minute);
    119     }
    120 
    121     public TimePicker(Context context) {
    122         this(context, null);
    123     }
    124 
    125     public TimePicker(Context context, AttributeSet attrs) {
    126         this(context, attrs, R.attr.timePickerStyle);
    127     }
    128 
    129     public TimePicker(Context context, AttributeSet attrs, int defStyle) {
    130         super(context, attrs, defStyle);
    131 
    132         // initialization based on locale
    133         setCurrentLocale(Locale.getDefault());
    134 
    135         // process style attributes
    136         TypedArray attributesArray = context.obtainStyledAttributes(
    137                 attrs, R.styleable.TimePicker, defStyle, 0);
    138         int layoutResourceId = attributesArray.getResourceId(
    139                 R.styleable.TimePicker_internalLayout, R.layout.time_picker);
    140         attributesArray.recycle();
    141 
    142         LayoutInflater inflater = (LayoutInflater) context.getSystemService(
    143                 Context.LAYOUT_INFLATER_SERVICE);
    144         inflater.inflate(layoutResourceId, this, true);
    145 
    146         // hour
    147         mHourSpinner = (NumberPicker) findViewById(R.id.hour);
    148         mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
    149             public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
    150                 updateInputState();
    151                 if (!is24HourView()) {
    152                     if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY)
    153                             || (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) {
    154                         mIsAm = !mIsAm;
    155                         updateAmPmControl();
    156                     }
    157                 }
    158                 onTimeChanged();
    159             }
    160         });
    161         mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input);
    162         mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
    163 
    164         // divider (only for the new widget style)
    165         mDivider = (TextView) findViewById(R.id.divider);
    166         if (mDivider != null) {
    167             mDivider.setText(R.string.time_picker_separator);
    168         }
    169 
    170         // minute
    171         mMinuteSpinner = (NumberPicker) findViewById(R.id.minute);
    172         mMinuteSpinner.setMinValue(0);
    173         mMinuteSpinner.setMaxValue(59);
    174         mMinuteSpinner.setOnLongPressUpdateInterval(100);
    175         mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
    176         mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
    177             public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
    178                 updateInputState();
    179                 int minValue = mMinuteSpinner.getMinValue();
    180                 int maxValue = mMinuteSpinner.getMaxValue();
    181                 if (oldVal == maxValue && newVal == minValue) {
    182                     int newHour = mHourSpinner.getValue() + 1;
    183                     if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) {
    184                         mIsAm = !mIsAm;
    185                         updateAmPmControl();
    186                     }
    187                     mHourSpinner.setValue(newHour);
    188                 } else if (oldVal == minValue && newVal == maxValue) {
    189                     int newHour = mHourSpinner.getValue() - 1;
    190                     if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) {
    191                         mIsAm = !mIsAm;
    192                         updateAmPmControl();
    193                     }
    194                     mHourSpinner.setValue(newHour);
    195                 }
    196                 onTimeChanged();
    197             }
    198         });
    199         mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input);
    200         mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
    201 
    202         /* Get the localized am/pm strings and use them in the spinner */
    203         mAmPmStrings = new DateFormatSymbols().getAmPmStrings();
    204 
    205         // am/pm
    206         View amPmView = findViewById(R.id.amPm);
    207         if (amPmView instanceof Button) {
    208             mAmPmSpinner = null;
    209             mAmPmSpinnerInput = null;
    210             mAmPmButton = (Button) amPmView;
    211             mAmPmButton.setOnClickListener(new OnClickListener() {
    212                 public void onClick(View button) {
    213                     button.requestFocus();
    214                     mIsAm = !mIsAm;
    215                     updateAmPmControl();
    216                     onTimeChanged();
    217                 }
    218             });
    219         } else {
    220             mAmPmButton = null;
    221             mAmPmSpinner = (NumberPicker) amPmView;
    222             mAmPmSpinner.setMinValue(0);
    223             mAmPmSpinner.setMaxValue(1);
    224             mAmPmSpinner.setDisplayedValues(mAmPmStrings);
    225             mAmPmSpinner.setOnValueChangedListener(new OnValueChangeListener() {
    226                 public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
    227                     updateInputState();
    228                     picker.requestFocus();
    229                     mIsAm = !mIsAm;
    230                     updateAmPmControl();
    231                     onTimeChanged();
    232                 }
    233             });
    234             mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input);
    235             mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
    236         }
    237 
    238         // update controls to initial state
    239         updateHourControl();
    240         updateAmPmControl();
    241 
    242         setOnTimeChangedListener(NO_OP_CHANGE_LISTENER);
    243 
    244         // set to current time
    245         setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY));
    246         setCurrentMinute(mTempCalendar.get(Calendar.MINUTE));
    247 
    248         if (!isEnabled()) {
    249             setEnabled(false);
    250         }
    251 
    252         // set the content descriptions
    253         setContentDescriptions();
    254 
    255         // If not explicitly specified this view is important for accessibility.
    256         if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
    257             setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    258         }
    259     }
    260 
    261     @Override
    262     public void setEnabled(boolean enabled) {
    263         if (mIsEnabled == enabled) {
    264             return;
    265         }
    266         super.setEnabled(enabled);
    267         mMinuteSpinner.setEnabled(enabled);
    268         if (mDivider != null) {
    269             mDivider.setEnabled(enabled);
    270         }
    271         mHourSpinner.setEnabled(enabled);
    272         if (mAmPmSpinner != null) {
    273             mAmPmSpinner.setEnabled(enabled);
    274         } else {
    275             mAmPmButton.setEnabled(enabled);
    276         }
    277         mIsEnabled = enabled;
    278     }
    279 
    280     @Override
    281     public boolean isEnabled() {
    282         return mIsEnabled;
    283     }
    284 
    285     @Override
    286     protected void onConfigurationChanged(Configuration newConfig) {
    287         super.onConfigurationChanged(newConfig);
    288         setCurrentLocale(newConfig.locale);
    289     }
    290 
    291     /**
    292      * Sets the current locale.
    293      *
    294      * @param locale The current locale.
    295      */
    296     private void setCurrentLocale(Locale locale) {
    297         if (locale.equals(mCurrentLocale)) {
    298             return;
    299         }
    300         mCurrentLocale = locale;
    301         mTempCalendar = Calendar.getInstance(locale);
    302     }
    303 
    304     /**
    305      * Used to save / restore state of time picker
    306      */
    307     private static class SavedState extends BaseSavedState {
    308 
    309         private final int mHour;
    310 
    311         private final int mMinute;
    312 
    313         private SavedState(Parcelable superState, int hour, int minute) {
    314             super(superState);
    315             mHour = hour;
    316             mMinute = minute;
    317         }
    318 
    319         private SavedState(Parcel in) {
    320             super(in);
    321             mHour = in.readInt();
    322             mMinute = in.readInt();
    323         }
    324 
    325         public int getHour() {
    326             return mHour;
    327         }
    328 
    329         public int getMinute() {
    330             return mMinute;
    331         }
    332 
    333         @Override
    334         public void writeToParcel(Parcel dest, int flags) {
    335             super.writeToParcel(dest, flags);
    336             dest.writeInt(mHour);
    337             dest.writeInt(mMinute);
    338         }
    339 
    340         @SuppressWarnings({"unused", "hiding"})
    341         public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
    342             public SavedState createFromParcel(Parcel in) {
    343                 return new SavedState(in);
    344             }
    345 
    346             public SavedState[] newArray(int size) {
    347                 return new SavedState[size];
    348             }
    349         };
    350     }
    351 
    352     @Override
    353     protected Parcelable onSaveInstanceState() {
    354         Parcelable superState = super.onSaveInstanceState();
    355         return new SavedState(superState, getCurrentHour(), getCurrentMinute());
    356     }
    357 
    358     @Override
    359     protected void onRestoreInstanceState(Parcelable state) {
    360         SavedState ss = (SavedState) state;
    361         super.onRestoreInstanceState(ss.getSuperState());
    362         setCurrentHour(ss.getHour());
    363         setCurrentMinute(ss.getMinute());
    364     }
    365 
    366     /**
    367      * Set the callback that indicates the time has been adjusted by the user.
    368      *
    369      * @param onTimeChangedListener the callback, should not be null.
    370      */
    371     public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) {
    372         mOnTimeChangedListener = onTimeChangedListener;
    373     }
    374 
    375     /**
    376      * @return The current hour in the range (0-23).
    377      */
    378     public Integer getCurrentHour() {
    379         int currentHour = mHourSpinner.getValue();
    380         if (is24HourView()) {
    381             return currentHour;
    382         } else if (mIsAm) {
    383             return currentHour % HOURS_IN_HALF_DAY;
    384         } else {
    385             return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
    386         }
    387     }
    388 
    389     /**
    390      * Set the current hour.
    391      */
    392     public void setCurrentHour(Integer currentHour) {
    393         // why was Integer used in the first place?
    394         if (currentHour == null || currentHour == getCurrentHour()) {
    395             return;
    396         }
    397         if (!is24HourView()) {
    398             // convert [0,23] ordinal to wall clock display
    399             if (currentHour >= HOURS_IN_HALF_DAY) {
    400                 mIsAm = false;
    401                 if (currentHour > HOURS_IN_HALF_DAY) {
    402                     currentHour = currentHour - HOURS_IN_HALF_DAY;
    403                 }
    404             } else {
    405                 mIsAm = true;
    406                 if (currentHour == 0) {
    407                     currentHour = HOURS_IN_HALF_DAY;
    408                 }
    409             }
    410             updateAmPmControl();
    411         }
    412         mHourSpinner.setValue(currentHour);
    413         onTimeChanged();
    414     }
    415 
    416     /**
    417      * Set whether in 24 hour or AM/PM mode.
    418      *
    419      * @param is24HourView True = 24 hour mode. False = AM/PM.
    420      */
    421     public void setIs24HourView(Boolean is24HourView) {
    422         if (mIs24HourView == is24HourView) {
    423             return;
    424         }
    425         mIs24HourView = is24HourView;
    426         // cache the current hour since spinner range changes
    427         int currentHour = getCurrentHour();
    428         updateHourControl();
    429         // set value after spinner range is updated
    430         setCurrentHour(currentHour);
    431         updateAmPmControl();
    432     }
    433 
    434     /**
    435      * @return true if this is in 24 hour view else false.
    436      */
    437     public boolean is24HourView() {
    438         return mIs24HourView;
    439     }
    440 
    441     /**
    442      * @return The current minute.
    443      */
    444     public Integer getCurrentMinute() {
    445         return mMinuteSpinner.getValue();
    446     }
    447 
    448     /**
    449      * Set the current minute (0-59).
    450      */
    451     public void setCurrentMinute(Integer currentMinute) {
    452         if (currentMinute == getCurrentMinute()) {
    453             return;
    454         }
    455         mMinuteSpinner.setValue(currentMinute);
    456         onTimeChanged();
    457     }
    458 
    459     @Override
    460     public int getBaseline() {
    461         return mHourSpinner.getBaseline();
    462     }
    463 
    464     @Override
    465     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    466         onPopulateAccessibilityEvent(event);
    467         return true;
    468     }
    469 
    470     @Override
    471     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    472         super.onPopulateAccessibilityEvent(event);
    473 
    474         int flags = DateUtils.FORMAT_SHOW_TIME;
    475         if (mIs24HourView) {
    476             flags |= DateUtils.FORMAT_24HOUR;
    477         } else {
    478             flags |= DateUtils.FORMAT_12HOUR;
    479         }
    480         mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour());
    481         mTempCalendar.set(Calendar.MINUTE, getCurrentMinute());
    482         String selectedDateUtterance = DateUtils.formatDateTime(mContext,
    483                 mTempCalendar.getTimeInMillis(), flags);
    484         event.getText().add(selectedDateUtterance);
    485     }
    486 
    487     @Override
    488     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    489         super.onInitializeAccessibilityEvent(event);
    490         event.setClassName(TimePicker.class.getName());
    491     }
    492 
    493     @Override
    494     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    495         super.onInitializeAccessibilityNodeInfo(info);
    496         info.setClassName(TimePicker.class.getName());
    497     }
    498 
    499     private void updateHourControl() {
    500         if (is24HourView()) {
    501             mHourSpinner.setMinValue(0);
    502             mHourSpinner.setMaxValue(23);
    503             mHourSpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
    504         } else {
    505             mHourSpinner.setMinValue(1);
    506             mHourSpinner.setMaxValue(12);
    507             mHourSpinner.setFormatter(null);
    508         }
    509     }
    510 
    511     private void updateAmPmControl() {
    512         if (is24HourView()) {
    513             if (mAmPmSpinner != null) {
    514                 mAmPmSpinner.setVisibility(View.GONE);
    515             } else {
    516                 mAmPmButton.setVisibility(View.GONE);
    517             }
    518         } else {
    519             int index = mIsAm ? Calendar.AM : Calendar.PM;
    520             if (mAmPmSpinner != null) {
    521                 mAmPmSpinner.setValue(index);
    522                 mAmPmSpinner.setVisibility(View.VISIBLE);
    523             } else {
    524                 mAmPmButton.setText(mAmPmStrings[index]);
    525                 mAmPmButton.setVisibility(View.VISIBLE);
    526             }
    527         }
    528         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
    529     }
    530 
    531     private void onTimeChanged() {
    532         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
    533         if (mOnTimeChangedListener != null) {
    534             mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute());
    535         }
    536     }
    537 
    538     private void setContentDescriptions() {
    539         // Minute
    540         trySetContentDescription(mMinuteSpinner, R.id.increment,
    541                 R.string.time_picker_increment_minute_button);
    542         trySetContentDescription(mMinuteSpinner, R.id.decrement,
    543                 R.string.time_picker_decrement_minute_button);
    544         // Hour
    545         trySetContentDescription(mHourSpinner, R.id.increment,
    546                 R.string.time_picker_increment_hour_button);
    547         trySetContentDescription(mHourSpinner, R.id.decrement,
    548                 R.string.time_picker_decrement_hour_button);
    549         // AM/PM
    550         if (mAmPmSpinner != null) {
    551             trySetContentDescription(mAmPmSpinner, R.id.increment,
    552                     R.string.time_picker_increment_set_pm_button);
    553             trySetContentDescription(mAmPmSpinner, R.id.decrement,
    554                     R.string.time_picker_decrement_set_am_button);
    555         }
    556     }
    557 
    558     private void trySetContentDescription(View root, int viewId, int contDescResId) {
    559         View target = root.findViewById(viewId);
    560         if (target != null) {
    561             target.setContentDescription(mContext.getString(contDescResId));
    562         }
    563     }
    564 
    565     private void updateInputState() {
    566         // Make sure that if the user changes the value and the IME is active
    567         // for one of the inputs if this widget, the IME is closed. If the user
    568         // changed the value via the IME and there is a next input the IME will
    569         // be shown, otherwise the user chose another means of changing the
    570         // value and having the IME up makes no sense.
    571         InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
    572         if (inputMethodManager != null) {
    573             if (inputMethodManager.isActive(mHourSpinnerInput)) {
    574                 mHourSpinnerInput.clearFocus();
    575                 inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
    576             } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) {
    577                 mMinuteSpinnerInput.clearFocus();
    578                 inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
    579             } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) {
    580                 mAmPmSpinnerInput.clearFocus();
    581                 inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
    582             }
    583         }
    584     }
    585 }
    586