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 17package android.support.v17.leanback.widget.picker; 18 19import android.content.Context; 20import android.content.res.TypedArray; 21import android.support.annotation.IntRange; 22import android.support.v17.leanback.R; 23import android.text.TextUtils; 24import android.text.format.DateFormat; 25import android.util.AttributeSet; 26import android.view.View; 27 28import java.text.SimpleDateFormat; 29import java.util.ArrayList; 30import java.util.Calendar; 31import java.util.List; 32import java.util.Locale; 33 34/** 35 * {@link TimePicker} is a direct subclass of {@link Picker}. 36 * <p> 37 * This class is a widget for selecting time and displays it according to the formatting for the 38 * current system locale. The time can be selected by hour, minute, and AM/PM picker columns. 39 * The AM/PM mode is determined by either explicitly setting the current mode through 40 * {@link #setIs24Hour(boolean)} or the widget attribute {@code is24HourFormat} (true for 24-hour 41 * mode, false for 12-hour mode). Otherwise, TimePicker retrieves the mode based on the current 42 * context. In 24-hour mode, TimePicker displays only the hour and minute columns. 43 * <p> 44 * This widget can show the current time as the initial value if {@code useCurrentTime} is set to 45 * true. Each individual time picker field can be set at any time by calling {@link #setHour(int)}, 46 * {@link #setMinute(int)} using 24-hour time format. The time format can also be changed at any 47 * time by calling {@link #setIs24Hour(boolean)}, and the AM/PM picker column will be activated or 48 * deactivated accordingly. 49 * 50 * @attr ref R.styleable#lbTimePicker_is24HourFormat 51 * @attr ref R.styleable#lbTimePicker_useCurrentTime 52 */ 53public class TimePicker extends Picker { 54 55 static final String TAG = "TimePicker"; 56 57 private static final int AM_INDEX = 0; 58 private static final int PM_INDEX = 1; 59 60 private static final int HOURS_IN_HALF_DAY = 12; 61 PickerColumn mHourColumn; 62 PickerColumn mMinuteColumn; 63 PickerColumn mAmPmColumn; 64 int mColHourIndex; 65 int mColMinuteIndex; 66 int mColAmPmIndex; 67 68 private final PickerUtility.TimeConstant mConstant; 69 70 private boolean mIs24hFormat; 71 72 private int mCurrentHour; 73 private int mCurrentMinute; 74 private int mCurrentAmPmIndex; 75 76 private String mTimePickerFormat; 77 78 /** 79 * Constructor called when inflating a TimePicker widget. This version uses a default style of 80 * 0, so the only attribute values applied are those in the Context's Theme and the given 81 * AttributeSet. 82 * 83 * @param context the context this TimePicker widget is associated with through which we can 84 * access the current theme attributes and resources 85 * @param attrs the attributes of the XML tag that is inflating the TimePicker widget 86 */ 87 public TimePicker(Context context, AttributeSet attrs) { 88 this(context, attrs, 0); 89 } 90 91 /** 92 * Constructor called when inflating a TimePicker widget. 93 * 94 * @param context the context this TimePicker widget is associated with through which we can 95 * access the current theme attributes and resources 96 * @param attrs the attributes of the XML tag that is inflating the TimePicker widget 97 * @param defStyleAttr An attribute in the current theme that contains a reference to a style 98 * resource that supplies default values for the widget. Can be 0 to not 99 * look for defaults. 100 */ 101 public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) { 102 super(context, attrs, defStyleAttr); 103 104 mConstant = PickerUtility.getTimeConstantInstance(Locale.getDefault(), 105 context.getResources()); 106 107 final TypedArray attributesArray = context.obtainStyledAttributes(attrs, 108 R.styleable.lbTimePicker); 109 mIs24hFormat = attributesArray.getBoolean(R.styleable.lbTimePicker_is24HourFormat, 110 DateFormat.is24HourFormat(context)); 111 boolean useCurrentTime = attributesArray.getBoolean(R.styleable.lbTimePicker_useCurrentTime, 112 true); 113 114 // The following 2 methods must be called after setting mIs24hFormat since this attribute is 115 // used to extract the time format string. 116 updateColumns(); 117 updateColumnsRange(); 118 119 if (useCurrentTime) { 120 Calendar currentDate = PickerUtility.getCalendarForLocale(null, 121 mConstant.locale); 122 setHour(currentDate.get(Calendar.HOUR_OF_DAY)); 123 setMinute(currentDate.get(Calendar.MINUTE)); 124 setAmPmValue(); 125 } 126 } 127 128 private static boolean updateMin(PickerColumn column, int value) { 129 if (value != column.getMinValue()) { 130 column.setMinValue(value); 131 return true; 132 } 133 return false; 134 } 135 136 private static boolean updateMax(PickerColumn column, int value) { 137 if (value != column.getMaxValue()) { 138 column.setMaxValue(value); 139 return true; 140 } 141 return false; 142 } 143 144 /** 145 * @return The best localized representation of time for the current locale 146 */ 147 String getBestHourMinutePattern() { 148 final String hourPattern; 149 if (PickerUtility.SUPPORTS_BEST_DATE_TIME_PATTERN) { 150 hourPattern = DateFormat.getBestDateTimePattern(mConstant.locale, mIs24hFormat ? "Hma" 151 : "hma"); 152 } else { 153 // Using short style to avoid picking extra fields e.g. time zone in the returned time 154 // format. 155 final java.text.DateFormat dateFormat = 156 SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT, mConstant.locale); 157 if (dateFormat instanceof SimpleDateFormat) { 158 String defaultPattern = ((SimpleDateFormat) dateFormat).toPattern(); 159 defaultPattern = defaultPattern.replace("s", ""); 160 if (mIs24hFormat) { 161 defaultPattern = defaultPattern.replace('h', 'H').replace("a", ""); 162 } 163 hourPattern = defaultPattern; 164 } else { 165 hourPattern = mIs24hFormat ? "H:mma" : "h:mma"; 166 } 167 } 168 return TextUtils.isEmpty(hourPattern) ? "h:mma" : hourPattern; 169 } 170 171 /** 172 * Extracts the separators used to separate time fields (including before the first and after 173 * the last time field). The separators can vary based on the individual locale and 12 or 174 * 24 hour time format, defined in the Unicode CLDR and cannot be supposed to be ":". 175 * 176 * See http://unicode.org/cldr/trac/browser/trunk/common/main 177 * 178 * For example, for english in 12 hour format 179 * (time pattern of "h:mm a"), this will return {"", ":", "", ""}, where the first separator 180 * indicates nothing needs to be displayed to the left of the hour field, ":" needs to be 181 * displayed to the right of hour field, and so forth. 182 * 183 * @return The ArrayList of separators to populate between the actual time fields in the 184 * TimePicker. 185 */ 186 List<CharSequence> extractSeparators() { 187 // Obtain the time format string per the current locale (e.g. h:mm a) 188 String hmaPattern = getBestHourMinutePattern(); 189 190 List<CharSequence> separators = new ArrayList<>(); 191 StringBuilder sb = new StringBuilder(); 192 char lastChar = '\0'; 193 // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats 194 final char[] timeFormats = {'H', 'h', 'K', 'k', 'm', 'M', 'a'}; 195 boolean processingQuote = false; 196 for (int i = 0; i < hmaPattern.length(); i++) { 197 char c = hmaPattern.charAt(i); 198 if (c == ' ') { 199 continue; 200 } 201 if (c == '\'') { 202 if (!processingQuote) { 203 sb.setLength(0); 204 processingQuote = true; 205 } else { 206 processingQuote = false; 207 } 208 continue; 209 } 210 if (processingQuote) { 211 sb.append(c); 212 } else { 213 if (isAnyOf(c, timeFormats)) { 214 if (c != lastChar) { 215 separators.add(sb.toString()); 216 sb.setLength(0); 217 } 218 } else { 219 sb.append(c); 220 } 221 } 222 lastChar = c; 223 } 224 separators.add(sb.toString()); 225 return separators; 226 } 227 228 private static boolean isAnyOf(char c, char[] any) { 229 for (int i = 0; i < any.length; i++) { 230 if (c == any[i]) { 231 return true; 232 } 233 } 234 return false; 235 } 236 237 /** 238 * 239 * @return the time picker format string based on the current system locale and the layout 240 * direction 241 */ 242 private String extractTimeFields() { 243 // Obtain the time format string per the current locale (e.g. h:mm a) 244 String hmaPattern = getBestHourMinutePattern(); 245 246 boolean isRTL = TextUtils.getLayoutDirectionFromLocale(mConstant.locale) == View 247 .LAYOUT_DIRECTION_RTL; 248 boolean isAmPmAtEnd = (hmaPattern.indexOf('a') >= 0) 249 ? (hmaPattern.indexOf("a") > hmaPattern.indexOf("m")) : true; 250 // Hour will always appear to the left of minutes regardless of layout direction. 251 String timePickerFormat = isRTL ? "mh" : "hm"; 252 253 if (is24Hour()) { 254 return timePickerFormat; 255 } else { 256 return isAmPmAtEnd ? (timePickerFormat + "a") : ("a" + timePickerFormat); 257 } 258 } 259 260 private void updateColumns() { 261 String timePickerFormat = getBestHourMinutePattern(); 262 if (TextUtils.equals(timePickerFormat, mTimePickerFormat)) { 263 return; 264 } 265 mTimePickerFormat = timePickerFormat; 266 267 String timeFieldsPattern = extractTimeFields(); 268 List<CharSequence> separators = extractSeparators(); 269 if (separators.size() != (timeFieldsPattern.length() + 1)) { 270 throw new IllegalStateException("Separators size: " + separators.size() + " must equal" 271 + " the size of timeFieldsPattern: " + timeFieldsPattern.length() + " + 1"); 272 } 273 setSeparators(separators); 274 timeFieldsPattern = timeFieldsPattern.toUpperCase(); 275 276 mHourColumn = mMinuteColumn = mAmPmColumn = null; 277 mColHourIndex = mColMinuteIndex = mColAmPmIndex = -1; 278 279 ArrayList<PickerColumn> columns = new ArrayList<>(3); 280 for (int i = 0; i < timeFieldsPattern.length(); i++) { 281 switch (timeFieldsPattern.charAt(i)) { 282 case 'H': 283 columns.add(mHourColumn = new PickerColumn()); 284 mHourColumn.setStaticLabels(mConstant.hours24); 285 mColHourIndex = i; 286 break; 287 case 'M': 288 columns.add(mMinuteColumn = new PickerColumn()); 289 mMinuteColumn.setStaticLabels(mConstant.minutes); 290 mColMinuteIndex = i; 291 break; 292 case 'A': 293 columns.add(mAmPmColumn = new PickerColumn()); 294 mAmPmColumn.setStaticLabels(mConstant.ampm); 295 mColAmPmIndex = i; 296 updateMin(mAmPmColumn, 0); 297 updateMax(mAmPmColumn, 1); 298 break; 299 default: 300 throw new IllegalArgumentException("Invalid time picker format."); 301 } 302 } 303 setColumns(columns); 304 } 305 306 private void updateColumnsRange() { 307 // updateHourColumn(false); 308 updateMin(mHourColumn, mIs24hFormat ? 0 : 1); 309 updateMax(mHourColumn, mIs24hFormat ? 23 : 12); 310 311 updateMin(mMinuteColumn, 0); 312 updateMax(mMinuteColumn, 59); 313 314 if (mAmPmColumn != null) { 315 updateMin(mAmPmColumn, 0); 316 updateMax(mAmPmColumn, 1); 317 } 318 } 319 320 /** 321 * Updates the value of AM/PM column for a 12 hour time format. The correct value should already 322 * be calculated before this method is called by calling setHour. 323 */ 324 private void setAmPmValue() { 325 if (!is24Hour()) { 326 setColumnValue(mColAmPmIndex, mCurrentAmPmIndex, false); 327 } 328 } 329 330 /** 331 * Sets the currently selected hour using a 24-hour time. 332 * 333 * @param hour the hour to set, in the range (0-23) 334 * @see #getHour() 335 */ 336 public void setHour(@IntRange(from = 0, to = 23) int hour) { 337 if (hour < 0 || hour > 23) { 338 throw new IllegalArgumentException("hour: " + hour + " is not in [0-23] range in"); 339 } 340 mCurrentHour = hour; 341 if (!is24Hour()) { 342 if (mCurrentHour >= HOURS_IN_HALF_DAY) { 343 mCurrentAmPmIndex = PM_INDEX; 344 if (mCurrentHour > HOURS_IN_HALF_DAY) { 345 mCurrentHour -= HOURS_IN_HALF_DAY; 346 } 347 } else { 348 mCurrentAmPmIndex = AM_INDEX; 349 if (mCurrentHour == 0) { 350 mCurrentHour = HOURS_IN_HALF_DAY; 351 } 352 } 353 setAmPmValue(); 354 } 355 setColumnValue(mColHourIndex, mCurrentHour, false); 356 } 357 358 /** 359 * Returns the currently selected hour using 24-hour time. 360 * 361 * @return the currently selected hour in the range (0-23) 362 * @see #setHour(int) 363 */ 364 public int getHour() { 365 if (mIs24hFormat) { 366 return mCurrentHour; 367 } 368 if (mCurrentAmPmIndex == AM_INDEX) { 369 return mCurrentHour % HOURS_IN_HALF_DAY; 370 } 371 return (mCurrentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; 372 } 373 374 /** 375 * Sets the currently selected minute. 376 * 377 * @param minute the minute to set, in the range (0-59) 378 * @see #getMinute() 379 */ 380 public void setMinute(@IntRange(from = 0, to = 59) int minute) { 381 if (minute < 0 || minute > 59) { 382 throw new IllegalArgumentException("minute: " + minute + " is not in [0-59] range."); 383 } 384 mCurrentMinute = minute; 385 setColumnValue(mColMinuteIndex, mCurrentMinute, false); 386 } 387 388 /** 389 * Returns the currently selected minute. 390 * 391 * @return the currently selected minute, in the range (0-59) 392 * @see #setMinute(int) 393 */ 394 public int getMinute() { 395 return mCurrentMinute; 396 } 397 398 /** 399 * Sets whether this widget displays a 24-hour mode or a 12-hour mode with an AM/PM picker. 400 * 401 * @param is24Hour {@code true} to display in 24-hour mode, 402 * {@code false} ti display in 12-hour mode with AM/PM. 403 * @see #is24Hour() 404 */ 405 public void setIs24Hour(boolean is24Hour) { 406 if (mIs24hFormat == is24Hour) { 407 return; 408 } 409 // the ordering of these statements is important 410 int currentHour = getHour(); 411 int currentMinute = getMinute(); 412 mIs24hFormat = is24Hour; 413 updateColumns(); 414 updateColumnsRange(); 415 416 setHour(currentHour); 417 setMinute(currentMinute); 418 setAmPmValue(); 419 } 420 421 /** 422 * @return {@code true} if this widget displays time in 24-hour mode, 423 * {@code false} otherwise. 424 * 425 * @see #setIs24Hour(boolean) 426 */ 427 public boolean is24Hour() { 428 return mIs24hFormat; 429 } 430 431 /** 432 * Only meaningful for a 12-hour time. 433 * 434 * @return {@code true} if the currently selected time is in PM, 435 * {@code false} if the currently selected time in in AM. 436 */ 437 public boolean isPm() { 438 return (mCurrentAmPmIndex == PM_INDEX); 439 } 440 441 @Override 442 public void onColumnValueChanged(int columnIndex, int newValue) { 443 if (columnIndex == mColHourIndex) { 444 mCurrentHour = newValue; 445 } else if (columnIndex == mColMinuteIndex) { 446 mCurrentMinute = newValue; 447 } else if (columnIndex == mColAmPmIndex) { 448 mCurrentAmPmIndex = newValue; 449 } else { 450 throw new IllegalArgumentException("Invalid column index."); 451 } 452 } 453} 454