• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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.support.v17.leanback.widget.picker;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.os.Build;
22 import android.support.annotation.IntRange;
23 import android.support.v17.leanback.R;
24 import android.text.TextUtils;
25 import android.text.format.DateFormat;
26 import android.util.AttributeSet;
27 import android.view.View;
28 import android.view.ViewGroup;
29 
30 import java.text.SimpleDateFormat;
31 import java.util.ArrayList;
32 import java.util.Calendar;
33 import java.util.Locale;
34 
35 /**
36  * {@link TimePicker} is a direct subclass of {@link Picker}.
37  * <p>
38  * This class is a widget for selecting time and displays it according to the formatting for the
39  * current system locale. The time can be selected by hour, minute, and AM/PM picker columns.
40  * The AM/PM mode is determined by either explicitly setting the current mode through
41  * {@link #setIs24Hour(boolean)} or the widget attribute {@code is24HourFormat} (true for 24-hour
42  * mode, false for 12-hour mode). Otherwise, TimePicker retrieves the mode based on the current
43  * context. In 24-hour mode, TimePicker displays only the hour and minute columns.
44  * <p>
45  * This widget can show the current time as the initial value if {@code useCurrentTime} is set to
46  * true. Each individual time picker field can be set at any time by calling {@link #setHour(int)},
47  * {@link #setMinute(int)} using 24-hour time format. The time format can also be changed at any
48  * time by calling {@link #setIs24Hour(boolean)}, and the AM/PM picker column will be activated or
49  * deactivated accordingly.
50  *
51  * @attr ref R.styleable#lbTimePicker_is24HourFormat
52  * @attr ref R.styleable#lbTimePicker_useCurrentTime
53  */
54 public class TimePicker extends Picker {
55 
56     static final String TAG = "TimePicker";
57 
58     private static final int AM_INDEX = 0;
59     private static final int PM_INDEX = 1;
60 
61     private static final int HOURS_IN_HALF_DAY = 12;
62     PickerColumn mHourColumn;
63     PickerColumn mMinuteColumn;
64     PickerColumn mAmPmColumn;
65     private ViewGroup mPickerView;
66     private View mAmPmSeparatorView;
67     int mColHourIndex;
68     int mColMinuteIndex;
69     int mColAmPmIndex;
70 
71     private final PickerUtility.TimeConstant mConstant;
72 
73     private boolean mIs24hFormat;
74 
75     private int mCurrentHour;
76     private int mCurrentMinute;
77     private int mCurrentAmPmIndex;
78 
79     /**
80      * Constructor called when inflating a TimePicker widget. This version uses a default style of
81      * 0, so the only attribute values applied are those in the Context's Theme and the given
82      * AttributeSet.
83      *
84      * @param context the context this TimePicker widget is associated with through which we can
85      *                access the current theme attributes and resources
86      * @param attrs the attributes of the XML tag that is inflating the TimePicker widget
87      */
TimePicker(Context context, AttributeSet attrs)88     public TimePicker(Context context, AttributeSet attrs) {
89         this(context, attrs, 0);
90     }
91 
92     /**
93      * Constructor called when inflating a TimePicker widget.
94      *
95      * @param context the context this TimePicker widget is associated with through which we can
96      *                access the current theme attributes and resources
97      * @param attrs the attributes of the XML tag that is inflating the TimePicker widget
98      * @param defStyleAttr An attribute in the current theme that contains a reference to a style
99      *                     resource that supplies default values for the widget. Can be 0 to not
100      *                     look for defaults.
101      */
TimePicker(Context context, AttributeSet attrs, int defStyleAttr)102     public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
103         super(context, attrs, defStyleAttr);
104 
105         mConstant = PickerUtility.getTimeConstantInstance(Locale.getDefault(),
106                 context.getResources());
107 
108         setSeparator(mConstant.timeSeparator);
109         mPickerView = findViewById(R.id.picker);
110         final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
111                 R.styleable.lbTimePicker);
112         mIs24hFormat = attributesArray.getBoolean(R.styleable.lbTimePicker_is24HourFormat,
113                 DateFormat.is24HourFormat(context));
114         boolean useCurrentTime = attributesArray.getBoolean(R.styleable.lbTimePicker_useCurrentTime,
115                 true);
116 
117         updateColumns(getTimePickerFormat());
118 
119         // The column range for the minute and AM/PM column is static and does not change, whereas
120         // the hour column range can change depending on whether 12 or 24 hour format is set at
121         // any given time.
122         updateHourColumn(false);
123         updateMin(mMinuteColumn, 0);
124         updateMax(mMinuteColumn, 59);
125 
126         updateMin(mAmPmColumn, 0);
127         updateMax(mAmPmColumn, 1);
128 
129         updateAmPmColumn();
130 
131         if (useCurrentTime) {
132             Calendar currentDate = PickerUtility.getCalendarForLocale(null,
133                     mConstant.locale);
134             setHour(currentDate.get(Calendar.HOUR_OF_DAY));
135             setMinute(currentDate.get(Calendar.MINUTE));
136         }
137     }
138 
updateMin(PickerColumn column, int value)139     private static boolean updateMin(PickerColumn column, int value) {
140         if (value != column.getMinValue()) {
141             column.setMinValue(value);
142             return true;
143         }
144         return false;
145     }
146 
updateMax(PickerColumn column, int value)147     private static boolean updateMax(PickerColumn column, int value) {
148         if (value != column.getMaxValue()) {
149             column.setMaxValue(value);
150             return true;
151         }
152         return false;
153     }
154 
155     /**
156      *
157      * @return the time picker format string based on the current system locale and the layout
158      *         direction
159      */
getTimePickerFormat()160     private String getTimePickerFormat() {
161         // Obtain the time format string per the current locale (e.g. h:mm a)
162         String hmaPattern;
163         if (Build.VERSION.SDK_INT >= 18) {
164             hmaPattern = DateFormat.getBestDateTimePattern(mConstant.locale, "hma");
165         } else {
166             // getTimeInstance is not very reliable and it may not include 'a' (for AM/PM)
167             // in the returned pattern string. In those cases, we assume that am/pm appears at the
168             // end of the fields. Need to find a more reliable way for API below 18.
169             hmaPattern  = ((SimpleDateFormat) java.text.DateFormat
170                     .getTimeInstance(java.text.DateFormat.FULL, mConstant.locale)).toPattern();
171         }
172 
173         boolean isRTL = TextUtils.getLayoutDirectionFromLocale(mConstant.locale) == View
174                 .LAYOUT_DIRECTION_RTL;
175         boolean isAmPmAtEnd = (hmaPattern.indexOf('a') >= 0)
176                 ? (hmaPattern.indexOf("a") > hmaPattern.indexOf("m")) : true;
177         // Hour will always appear to the left of minutes regardless of layout direction.
178         String timePickerFormat = isRTL ? "mh" : "hm";
179 
180         return isAmPmAtEnd ? (timePickerFormat + "a") : ("a" + timePickerFormat);
181     }
182 
updateColumns(String timePickerFormat)183     private void updateColumns(String timePickerFormat) {
184         if (TextUtils.isEmpty(timePickerFormat)) {
185             timePickerFormat = "hma";
186         }
187         timePickerFormat = timePickerFormat.toUpperCase();
188 
189         mHourColumn = mMinuteColumn = mAmPmColumn = null;
190         mColHourIndex = mColMinuteIndex = mColAmPmIndex = -1;
191 
192         ArrayList<PickerColumn> columns = new ArrayList<>(3);
193         for (int i = 0; i < timePickerFormat.length(); i++) {
194             switch (timePickerFormat.charAt(i)) {
195                 case 'H':
196                     columns.add(mHourColumn = new PickerColumn());
197                     mHourColumn.setStaticLabels(mConstant.hours24);
198                     mColHourIndex = i;
199                     break;
200                 case 'M':
201                     columns.add(mMinuteColumn = new PickerColumn());
202                     mMinuteColumn.setStaticLabels(mConstant.minutes);
203                     mColMinuteIndex = i;
204                     break;
205                 case 'A':
206                     columns.add(mAmPmColumn = new PickerColumn());
207                     mAmPmColumn.setStaticLabels(mConstant.ampm);
208                     mColAmPmIndex = i;
209                     updateMin(mAmPmColumn, 0);
210                     updateMax(mAmPmColumn, 1);
211                     break;
212                 default:
213                     throw new IllegalArgumentException("Invalid time picker format.");
214             }
215         }
216         setColumns(columns);
217         mAmPmSeparatorView = mPickerView.getChildAt(mColAmPmIndex == 0 ? 1 :
218                 (2 * mColAmPmIndex - 1));
219     }
220 
221     /**
222      * Updates the range in the hour column and notifies column changed if notifyChanged is true.
223      * Hour column can have either [0-23] or [1-12] depending on whether the 24 hour format is set
224      * or not.
225      *
226      * @param notifyChanged {code true} if we should notify data set changed on the hour column,
227      *                      {@code false} otherwise.
228      */
updateHourColumn(boolean notifyChanged)229     private void updateHourColumn(boolean notifyChanged) {
230         updateMin(mHourColumn, mIs24hFormat ? 0 : 1);
231         updateMax(mHourColumn, mIs24hFormat ? 23 : 12);
232         if (notifyChanged) {
233             setColumnAt(mColHourIndex, mHourColumn);
234         }
235     }
236 
237     /**
238      * Updates AM/PM column depending on whether the 24 hour format is set or not. The visibility of
239      * this column is set to {@code GONE} for a 24 hour format, and {@code VISIBLE} in 12 hour
240      * format. This method also updates the value of this column for a 12 hour format.
241      */
updateAmPmColumn()242     private void updateAmPmColumn() {
243         if (mIs24hFormat) {
244             mColumnViews.get(mColAmPmIndex).setVisibility(GONE);
245             mAmPmSeparatorView.setVisibility(GONE);
246         } else {
247             mColumnViews.get(mColAmPmIndex).setVisibility(VISIBLE);
248             mAmPmSeparatorView.setVisibility(VISIBLE);
249             setColumnValue(mColAmPmIndex, mCurrentAmPmIndex, false);
250         }
251     }
252 
253     /**
254      * Sets the currently selected hour using a 24-hour time.
255      *
256      * @param hour the hour to set, in the range (0-23)
257      * @see #getHour()
258      */
setHour(@ntRangefrom = 0, to = 23) int hour)259     public void setHour(@IntRange(from = 0, to = 23) int hour) {
260         if (hour < 0 || hour > 23) {
261             throw new IllegalArgumentException("hour: " + hour + " is not in [0-23] range in");
262         }
263         mCurrentHour = hour;
264         if (!mIs24hFormat) {
265             if (mCurrentHour >= HOURS_IN_HALF_DAY) {
266                 mCurrentAmPmIndex = PM_INDEX;
267                 if (mCurrentHour > HOURS_IN_HALF_DAY) {
268                     mCurrentHour -= HOURS_IN_HALF_DAY;
269                 }
270             } else {
271                 mCurrentAmPmIndex = AM_INDEX;
272                 if (mCurrentHour == 0) {
273                     mCurrentHour = HOURS_IN_HALF_DAY;
274                 }
275             }
276             updateAmPmColumn();
277         }
278         setColumnValue(mColHourIndex, mCurrentHour, false);
279     }
280 
281     /**
282      * Returns the currently selected hour using 24-hour time.
283      *
284      * @return the currently selected hour in the range (0-23)
285      * @see #setHour(int)
286      */
getHour()287     public int getHour() {
288         if (mIs24hFormat) {
289             return mCurrentHour;
290         }
291         if (mCurrentAmPmIndex == AM_INDEX) {
292             return mCurrentHour % HOURS_IN_HALF_DAY;
293         }
294         return (mCurrentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
295     }
296 
297     /**
298      * Sets the currently selected minute.
299      *
300      * @param minute the minute to set, in the range (0-59)
301      * @see #getMinute()
302      */
setMinute(@ntRangefrom = 0, to = 59) int minute)303     public void setMinute(@IntRange(from = 0, to = 59) int minute) {
304         if (mCurrentMinute == minute) {
305             return;
306         }
307         if (minute < 0 || minute > 59) {
308             throw new IllegalArgumentException("minute: " + minute + " is not in [0-59] range.");
309         }
310         mCurrentMinute = minute;
311         setColumnValue(mColMinuteIndex, mCurrentMinute, false);
312     }
313 
314     /**
315      * Returns the currently selected minute.
316      *
317      * @return the currently selected minute, in the range (0-59)
318      * @see #setMinute(int)
319      */
getMinute()320     public int getMinute() {
321         return mCurrentMinute;
322     }
323 
324     /**
325      * Sets whether this widget displays a 24-hour mode or a 12-hour mode with an AM/PM picker.
326      *
327      * @param is24Hour {@code true} to display in 24-hour mode,
328      *                 {@code false} ti display in 12-hour mode with AM/PM.
329      * @see #is24Hour()
330      */
setIs24Hour(boolean is24Hour)331     public void setIs24Hour(boolean is24Hour) {
332         if (mIs24hFormat == is24Hour) {
333             return;
334         }
335         // the ordering of these statements is important
336         int currentHour = getHour();
337         mIs24hFormat = is24Hour;
338         updateHourColumn(true);
339         setHour(currentHour);
340         updateAmPmColumn();
341     }
342 
343     /**
344      * @return {@code true} if this widget displays time in 24-hour mode,
345      *         {@code false} otherwise.
346      *
347      * @see #setIs24Hour(boolean)
348      */
is24Hour()349     public boolean is24Hour() {
350         return mIs24hFormat;
351     }
352 
353     /**
354      * Only meaningful for a 12-hour time.
355      *
356      * @return {@code true} if the currently selected time is in PM,
357      *         {@code false} if the currently selected time in in AM.
358      */
isPm()359     public boolean isPm() {
360         return (mCurrentAmPmIndex == PM_INDEX);
361     }
362 
363     @Override
onColumnValueChanged(int columnIndex, int newValue)364     public void onColumnValueChanged(int columnIndex, int newValue) {
365         if (columnIndex == mColHourIndex) {
366             mCurrentHour = newValue;
367         } else if (columnIndex == mColMinuteIndex) {
368             mCurrentMinute = newValue;
369         } else if (columnIndex == mColAmPmIndex) {
370             mCurrentAmPmIndex = newValue;
371         } else {
372             throw new IllegalArgumentException("Invalid column index.");
373         }
374     }
375 }
376