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