DatePicker.java revision c39d9c75590eca86a5e7e32a8824ba04a0d42e9b
1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 15package android.support.v17.leanback.widget.picker; 16 17import android.content.Context; 18import android.content.res.TypedArray; 19import android.support.annotation.RestrictTo; 20import android.support.v17.leanback.R; 21import android.text.TextUtils; 22import android.util.AttributeSet; 23import android.util.Log; 24 25import java.text.DateFormat; 26import java.text.ParseException; 27import java.text.SimpleDateFormat; 28import java.util.ArrayList; 29import java.util.Calendar; 30import java.util.Locale; 31import java.util.TimeZone; 32 33import static android.support.annotation.RestrictTo.Scope.GROUP_ID; 34 35/** 36 * {@link DatePicker} is a directly subclass of {@link Picker}. 37 * This class is a widget for selecting a date. The date can be selected by a 38 * year, month, and day Columns. The "minDate" and "maxDate" from which dates to be selected 39 * can be customized. The columns can be customized by attribute "datePickerFormat" or 40 * {@link #setDatePickerFormat(String)}. 41 * 42 * @attr ref R.styleable#lbDatePicker_android_maxDate 43 * @attr ref R.styleable#lbDatePicker_android_minDate 44 * @attr ref R.styleable#lbDatePicker_datePickerFormat 45 * @hide 46 */ 47@RestrictTo(GROUP_ID) 48public class DatePicker extends Picker { 49 50 static final String LOG_TAG = "DatePicker"; 51 52 private String mDatePickerFormat; 53 PickerColumn mMonthColumn; 54 PickerColumn mDayColumn; 55 PickerColumn mYearColumn; 56 int mColMonthIndex; 57 int mColDayIndex; 58 int mColYearIndex; 59 60 final static String DATE_FORMAT = "MM/dd/yyyy"; 61 final DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); 62 PickerConstant mConstant; 63 64 Calendar mMinDate; 65 Calendar mMaxDate; 66 Calendar mCurrentDate; 67 Calendar mTempDate; 68 69 public DatePicker(Context context, AttributeSet attrs) { 70 this(context, attrs, 0); 71 } 72 73 public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) { 74 super(context, attrs, defStyleAttr); 75 76 updateCurrentLocale(); 77 setSeparator(mConstant.dateSeparator); 78 79 final TypedArray attributesArray = context.obtainStyledAttributes(attrs, 80 R.styleable.lbDatePicker); 81 String minDate = attributesArray.getString(R.styleable.lbDatePicker_android_minDate); 82 String maxDate = attributesArray.getString(R.styleable.lbDatePicker_android_maxDate); 83 mTempDate.clear(); 84 if (!TextUtils.isEmpty(minDate)) { 85 if (!parseDate(minDate, mTempDate)) { 86 mTempDate.set(1900, 0, 1); 87 } 88 } else { 89 mTempDate.set(1900, 0, 1); 90 } 91 mMinDate.setTimeInMillis(mTempDate.getTimeInMillis()); 92 93 mTempDate.clear(); 94 if (!TextUtils.isEmpty(maxDate)) { 95 if (!parseDate(maxDate, mTempDate)) { 96 mTempDate.set(2100, 0, 1); 97 } 98 } else { 99 mTempDate.set(2100, 0, 1); 100 } 101 mMaxDate.setTimeInMillis(mTempDate.getTimeInMillis()); 102 103 String datePickerFormat = attributesArray 104 .getString(R.styleable.lbDatePicker_datePickerFormat); 105 if (TextUtils.isEmpty(datePickerFormat)) { 106 datePickerFormat = new String( 107 android.text.format.DateFormat.getDateFormatOrder(context)); 108 } 109 setDatePickerFormat(datePickerFormat); 110 } 111 112 private boolean parseDate(String date, Calendar outDate) { 113 try { 114 outDate.setTime(mDateFormat.parse(date)); 115 return true; 116 } catch (ParseException e) { 117 Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); 118 return false; 119 } 120 } 121 122 /** 123 * Changes format of showing dates. For example "YMD". 124 * @param datePickerFormat Format of showing dates. 125 */ 126 public void setDatePickerFormat(String datePickerFormat) { 127 if (TextUtils.isEmpty(datePickerFormat)) { 128 datePickerFormat = new String( 129 android.text.format.DateFormat.getDateFormatOrder(getContext())); 130 } 131 datePickerFormat = datePickerFormat.toUpperCase(); 132 if (TextUtils.equals(mDatePickerFormat, datePickerFormat)) { 133 return; 134 } 135 mDatePickerFormat = datePickerFormat; 136 mYearColumn = mMonthColumn = mDayColumn = null; 137 mColYearIndex = mColDayIndex = mColMonthIndex = -1; 138 ArrayList<PickerColumn> columns = new ArrayList<PickerColumn>(3); 139 for (int i = 0; i < datePickerFormat.length(); i++) { 140 switch (datePickerFormat.charAt(i)) { 141 case 'Y': 142 if (mYearColumn != null) { 143 throw new IllegalArgumentException("datePicker format error"); 144 } 145 columns.add(mYearColumn = new PickerColumn()); 146 mColYearIndex = i; 147 mYearColumn.setLabelFormat("%d"); 148 break; 149 case 'M': 150 if (mMonthColumn != null) { 151 throw new IllegalArgumentException("datePicker format error"); 152 } 153 columns.add(mMonthColumn = new PickerColumn()); 154 mMonthColumn.setStaticLabels(mConstant.months); 155 mColMonthIndex = i; 156 break; 157 case 'D': 158 if (mDayColumn != null) { 159 throw new IllegalArgumentException("datePicker format error"); 160 } 161 columns.add(mDayColumn = new PickerColumn()); 162 mDayColumn.setLabelFormat("%02d"); 163 mColDayIndex = i; 164 break; 165 default: 166 throw new IllegalArgumentException("datePicker format error"); 167 } 168 } 169 setColumns(columns); 170 updateSpinners(false); 171 } 172 173 /** 174 * Get format of showing dates. For example "YMD". Default value is from 175 * {@link android.text.format.DateFormat#getDateFormatOrder(Context)}. 176 * @return Format of showing dates. 177 */ 178 public String getDatePickerFormat() { 179 return mDatePickerFormat; 180 } 181 182 private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) { 183 if (oldCalendar == null) { 184 return Calendar.getInstance(locale); 185 } else { 186 final long currentTimeMillis = oldCalendar.getTimeInMillis(); 187 Calendar newCalendar = Calendar.getInstance(locale); 188 newCalendar.setTimeInMillis(currentTimeMillis); 189 return newCalendar; 190 } 191 } 192 193 private void updateCurrentLocale() { 194 mConstant = new PickerConstant(Locale.getDefault(), getContext().getResources()); 195 mTempDate = getCalendarForLocale(mTempDate, mConstant.locale); 196 mMinDate = getCalendarForLocale(mMinDate, mConstant.locale); 197 mMaxDate = getCalendarForLocale(mMaxDate, mConstant.locale); 198 mCurrentDate = getCalendarForLocale(mCurrentDate, mConstant.locale); 199 200 if (mMonthColumn != null) { 201 mMonthColumn.setStaticLabels(mConstant.months); 202 setColumnAt(mColMonthIndex, mMonthColumn); 203 } 204 } 205 206 @Override 207 public final void onColumnValueChanged(int column, int newVal) { 208 mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis()); 209 // take care of wrapping of days and months to update greater fields 210 int oldVal = getColumnAt(column).getCurrentValue(); 211 if (column == mColDayIndex) { 212 mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal); 213 } else if (column == mColMonthIndex) { 214 mTempDate.add(Calendar.MONTH, newVal - oldVal); 215 } else if (column == mColYearIndex) { 216 mTempDate.add(Calendar.YEAR, newVal - oldVal); 217 } else { 218 throw new IllegalArgumentException(); 219 } 220 setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH), 221 mTempDate.get(Calendar.DAY_OF_MONTH)); 222 updateSpinners(false); 223 } 224 225 226 /** 227 * Sets the minimal date supported by this {@link DatePicker} in 228 * milliseconds since January 1, 1970 00:00:00 in 229 * {@link TimeZone#getDefault()} time zone. 230 * 231 * @param minDate The minimal supported date. 232 */ 233 public void setMinDate(long minDate) { 234 mTempDate.setTimeInMillis(minDate); 235 if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) 236 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { 237 return; 238 } 239 mMinDate.setTimeInMillis(minDate); 240 if (mCurrentDate.before(mMinDate)) { 241 mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); 242 } 243 updateSpinners(false); 244 } 245 246 247 /** 248 * Gets the minimal date supported by this {@link DatePicker} in 249 * milliseconds since January 1, 1970 00:00:00 in 250 * {@link TimeZone#getDefault()} time zone. 251 * <p> 252 * Note: The default minimal date is 01/01/1900. 253 * <p> 254 * 255 * @return The minimal supported date. 256 */ 257 public long getMinDate() { 258 return mMinDate.getTimeInMillis(); 259 } 260 261 /** 262 * Sets the maximal date supported by this {@link DatePicker} in 263 * milliseconds since January 1, 1970 00:00:00 in 264 * {@link TimeZone#getDefault()} time zone. 265 * 266 * @param maxDate The maximal supported date. 267 */ 268 public void setMaxDate(long maxDate) { 269 mTempDate.setTimeInMillis(maxDate); 270 if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) 271 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) { 272 return; 273 } 274 mMaxDate.setTimeInMillis(maxDate); 275 if (mCurrentDate.after(mMaxDate)) { 276 mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); 277 } 278 updateSpinners(false); 279 } 280 281 /** 282 * Gets the maximal date supported by this {@link DatePicker} in 283 * milliseconds since January 1, 1970 00:00:00 in 284 * {@link TimeZone#getDefault()} time zone. 285 * <p> 286 * Note: The default maximal date is 12/31/2100. 287 * <p> 288 * 289 * @return The maximal supported date. 290 */ 291 public long getMaxDate() { 292 return mMaxDate.getTimeInMillis(); 293 } 294 295 /** 296 * Gets current date value in milliseconds since January 1, 1970 00:00:00 in 297 * {@link TimeZone#getDefault()} time zone. 298 * 299 * @return Current date values. 300 */ 301 public long getDate() { 302 return mCurrentDate.getTimeInMillis(); 303 } 304 305 private void setDate(int year, int month, int dayOfMonth) { 306 mCurrentDate.set(year, month, dayOfMonth); 307 if (mCurrentDate.before(mMinDate)) { 308 mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); 309 } else if (mCurrentDate.after(mMaxDate)) { 310 mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); 311 } 312 } 313 314 /** 315 * Update the current date. 316 * 317 * @param year The year. 318 * @param month The month which is <strong>starting from zero</strong>. 319 * @param dayOfMonth The day of the month. 320 * @param animation True to run animation to scroll the column. 321 */ 322 public void updateDate(int year, int month, int dayOfMonth, boolean animation) { 323 if (!isNewDate(year, month, dayOfMonth)) { 324 return; 325 } 326 setDate(year, month, dayOfMonth); 327 updateSpinners(animation); 328 } 329 330 private boolean isNewDate(int year, int month, int dayOfMonth) { 331 return (mCurrentDate.get(Calendar.YEAR) != year 332 || mCurrentDate.get(Calendar.MONTH) != dayOfMonth 333 || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month); 334 } 335 336 private static boolean updateMin(PickerColumn column, int value) { 337 if (value != column.getMinValue()) { 338 column.setMinValue(value); 339 return true; 340 } 341 return false; 342 } 343 344 private static boolean updateMax(PickerColumn column, int value) { 345 if (value != column.getMaxValue()) { 346 column.setMaxValue(value); 347 return true; 348 } 349 return false; 350 } 351 352 private static int[] DATE_FIELDS = {Calendar.DAY_OF_MONTH, Calendar.MONTH, Calendar.YEAR}; 353 354 // Following implementation always keeps up-to-date date ranges (min & max values) no matter 355 // what the currently selected date is. This prevents the constant updating of date values while 356 // scrolling vertically and thus fixes the animation jumps that used to happen when we reached 357 // the endpoint date field values since the adapter values do not change while scrolling up 358 // & down across a single field. 359 void updateSpinnersImpl(boolean animation) { 360 // set the spinner ranges respecting the min and max dates 361 int dateFieldIndices[] = {mColDayIndex, mColMonthIndex, mColYearIndex}; 362 363 boolean allLargerDateFieldsHaveBeenEqualToMinDate = true; 364 boolean allLargerDateFieldsHaveBeenEqualToMaxDate = true; 365 for(int i = DATE_FIELDS.length - 1; i >= 0; i--) { 366 boolean dateFieldChanged = false; 367 if (dateFieldIndices[i] < 0) 368 continue; 369 370 int currField = DATE_FIELDS[i]; 371 PickerColumn currPickerColumn = getColumnAt(dateFieldIndices[i]); 372 373 if (allLargerDateFieldsHaveBeenEqualToMinDate) { 374 dateFieldChanged |= updateMin(currPickerColumn, 375 mMinDate.get(currField)); 376 } else { 377 dateFieldChanged |= updateMin(currPickerColumn, 378 mCurrentDate.getActualMinimum(currField)); 379 } 380 381 if (allLargerDateFieldsHaveBeenEqualToMaxDate) { 382 dateFieldChanged |= updateMax(currPickerColumn, 383 mMaxDate.get(currField)); 384 } else { 385 dateFieldChanged |= updateMax(currPickerColumn, 386 mCurrentDate.getActualMaximum(currField)); 387 } 388 389 allLargerDateFieldsHaveBeenEqualToMinDate &= 390 (mCurrentDate.get(currField) == mMinDate.get(currField)); 391 allLargerDateFieldsHaveBeenEqualToMaxDate &= 392 (mCurrentDate.get(currField) == mMaxDate.get(currField)); 393 394 if (dateFieldChanged) { 395 setColumnAt(dateFieldIndices[i], currPickerColumn); 396 } 397 setColumnValue(dateFieldIndices[i], mCurrentDate.get(currField), animation); 398 } 399 } 400 401 private void updateSpinners(final boolean animation) { 402 // update range in a post call. The reason is that RV does not allow notifyDataSetChange() 403 // in scroll pass. UpdateSpinner can be called in a scroll pass, UpdateSpinner() may 404 // notifyDataSetChange to update the range. 405 post(new Runnable() { 406 @Override 407 public void run() { 408 updateSpinnersImpl(animation); 409 } 410 }); 411 } 412}