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