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