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