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