• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}resources/tutorials/views/hello-timepicker.html">Time Picker
52  * tutorial</a>.
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          */
onTimeChanged(TimePicker view, int hourOfDay, int minute)118         void onTimeChanged(TimePicker view, int hourOfDay, int minute);
119     }
120 
TimePicker(Context context)121     public TimePicker(Context context) {
122         this(context, null);
123     }
124 
TimePicker(Context context, AttributeSet attrs)125     public TimePicker(Context context, AttributeSet attrs) {
126         this(context, attrs, R.attr.timePickerStyle);
127     }
128 
TimePicker(Context context, AttributeSet attrs, int defStyle)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.TWO_DIGIT_FORMATTER);
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
setEnabled(boolean enabled)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
isEnabled()281     public boolean isEnabled() {
282         return mIsEnabled;
283     }
284 
285     @Override
onConfigurationChanged(Configuration newConfig)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      */
setCurrentLocale(Locale locale)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 
SavedState(Parcelable superState, int hour, int minute)313         private SavedState(Parcelable superState, int hour, int minute) {
314             super(superState);
315             mHour = hour;
316             mMinute = minute;
317         }
318 
SavedState(Parcel in)319         private SavedState(Parcel in) {
320             super(in);
321             mHour = in.readInt();
322             mMinute = in.readInt();
323         }
324 
getHour()325         public int getHour() {
326             return mHour;
327         }
328 
getMinute()329         public int getMinute() {
330             return mMinute;
331         }
332 
333         @Override
writeToParcel(Parcel dest, int flags)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
onSaveInstanceState()353     protected Parcelable onSaveInstanceState() {
354         Parcelable superState = super.onSaveInstanceState();
355         return new SavedState(superState, getCurrentHour(), getCurrentMinute());
356     }
357 
358     @Override
onRestoreInstanceState(Parcelable state)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      */
setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener)371     public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) {
372         mOnTimeChangedListener = onTimeChangedListener;
373     }
374 
375     /**
376      * @return The current hour in the range (0-23).
377      */
getCurrentHour()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      */
setCurrentHour(Integer currentHour)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      */
setIs24HourView(Boolean is24HourView)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      */
is24HourView()437     public boolean is24HourView() {
438         return mIs24HourView;
439     }
440 
441     /**
442      * @return The current minute.
443      */
getCurrentMinute()444     public Integer getCurrentMinute() {
445         return mMinuteSpinner.getValue();
446     }
447 
448     /**
449      * Set the current minute (0-59).
450      */
setCurrentMinute(Integer currentMinute)451     public void setCurrentMinute(Integer currentMinute) {
452         if (currentMinute == getCurrentMinute()) {
453             return;
454         }
455         mMinuteSpinner.setValue(currentMinute);
456         onTimeChanged();
457     }
458 
459     @Override
getBaseline()460     public int getBaseline() {
461         return mHourSpinner.getBaseline();
462     }
463 
464     @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)465     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
466         onPopulateAccessibilityEvent(event);
467         return true;
468     }
469 
470     @Override
onPopulateAccessibilityEvent(AccessibilityEvent event)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
onInitializeAccessibilityEvent(AccessibilityEvent event)488     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
489         super.onInitializeAccessibilityEvent(event);
490         event.setClassName(TimePicker.class.getName());
491     }
492 
493     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)494     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
495         super.onInitializeAccessibilityNodeInfo(info);
496         info.setClassName(TimePicker.class.getName());
497     }
498 
updateHourControl()499     private void updateHourControl() {
500         if (is24HourView()) {
501             mHourSpinner.setMinValue(0);
502             mHourSpinner.setMaxValue(23);
503             mHourSpinner.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
504         } else {
505             mHourSpinner.setMinValue(1);
506             mHourSpinner.setMaxValue(12);
507             mHourSpinner.setFormatter(null);
508         }
509     }
510 
updateAmPmControl()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 
onTimeChanged()531     private void onTimeChanged() {
532         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
533         if (mOnTimeChangedListener != null) {
534             mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute());
535         }
536     }
537 
setContentDescriptions()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 
trySetContentDescription(View root, int viewId, int contDescResId)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 
updateInputState()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